Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stream: Expose DuplexPair API #34111

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

awwright
Copy link
Contributor

@awwright awwright commented Jun 29, 2020

The "Duplex Pair" pattern is a function that creates two symmetrical Duplex streams, such that what is written on one side will become readable on the other.

This pattern is used by the tls module in core, and in a great number of the tests. This PR merges the two implementations together into a single stream.DuplexPair implementation.

See #29986 for a discussion of this feature.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs related to general changes in the lib or src directory. label Jun 29, 2020
@awwright awwright changed the title Expose DuplexPair API Jun 29, 2020
@mscdex
Copy link
Contributor

mscdex commented Jun 29, 2020

Why should we be exposing this publicly? Is this a common use case? Is there a reference issue with more information?

@awwright
Copy link
Contributor Author

awwright commented Jun 29, 2020

@mscdex I think it's a much more familiar way of creating a Duplex stream for another party, instead of having to use the push/_write/_read/_destroy/_final API. I think there'd be a great benefit if more people moved in the direction of creating a stream using a symmetrical API... see #29986 for a discussion; @addaleax suggested filing a PR over there.

@@ -14,7 +14,7 @@ const tick = require('../common/tick');
{
// This creates a session and schedules a write (for the settings frame).
let client = http2.connect('http://localhost:80', {
createConnection: common.mustCall(() => makeDuplexPair().clientSide)
createConnection: common.mustCall(() => new DuplexPair().clientSide)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would anyone like to speculate why this test creates a duplex pair and then never uses the other side?

const clientSide = new DuplexSocket();
const serverSide = new DuplexSocket();
function DuplexPair() {
if (!(this instanceof DuplexPair))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid adding this for new APIs? We may as well use a class, which would force new anyway.

Copy link
Contributor Author

@awwright awwright Jun 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll check it out.
Does Node.js do it this way anywhere else? I was trying to copy how the other stream classes work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use class for almost everything since class has been introduced into the language. Unfortunately, we cannot convert this old-style pattern to ES6 classes without a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax Would it be possible to get a deprecation notice for that, then? Or is there some documentation on what causes the breaking change? (My understanding was that class is just syntactic sugar.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax Would it be possible to get a deprecation notice for that, then?

I don’t think this is something that we’re ever realistically going to be able to change, so there’s no real reason to deprecated it, I assume.

Or is there some documentation on what causes the breaking change? (My understanding was that class is just syntactic sugar.)

It’s the fact that classes have to be invoked with new, and will throw otherwise. That”s not the case in the current code here.

Copy link
Contributor Author

@awwright awwright Jun 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax Thanks! I'm working on that now.

Sort of off-topic follow-up, is calling builtins without new discouraged too then? e.g. const now = Date();? I've been in discussions with a couple core members on the IRC channel, who've said more-or-less "new is obsolete these days" but I assume that was just a personal opinion around various programming styles.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@awwright Date is special, because Date() and new Date() do different things :)

But yes, for a function that implements a pattern like this (returns an instance of itself even when called without new), I would recommend using new because that allows transitioning to classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying this code:

class DuplexPair {
  [Symbol.iterator] = Array.prototype[Symbol.iterator];
  length = 2;
  constructor() {
    const clientSide = this[0] = new DuplexSide();
    const serverSide = this[1] = new DuplexSide();
    this.serverSide = clientSide[kOtherSide] = serverSide;
    this.clientSide = serverSide[kOtherSide] = clientSide;
  }
}

This passes the test-release suite, but the acorn dependency can't grok it ("SyntaxError: unexpected token (42:20)" which is at the first closing square bracket). Am I missing something here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@awwright Yeah, we’ve run into trouble with acorn in the past for private properties as well, so I guess this falls into a similar category :/ Fwiw, I think if you do this, you could also use class DuplexPair extends Array, to automatically get length and the iterator property…

@mscdex
Copy link
Contributor

mscdex commented Jun 29, 2020

If we're going to make this public, I think we should be using more generic property names instead of 'client' and 'server' since it's no longer specific to tls.

@awwright
Copy link
Contributor Author

@mscdex I considered exposing socket1 and socket2, which is what tls uses (the test suite uses clientSide and serverSide). I settled on additionally exposing an array called pair so that you can pick any name you want:

const [ socket1, socket2 ] = new DuplexPair().pair;

This is what TLS now does, in the patch. Do you think that's sufficient?

Some other ideas:

  • make the while object iterable, so you can do e.g. const [ a, b ] = DuplexPair();
  • socket1 and socket2 (what tls uses)
  • upstream and downstream
  • inside and outside
  • a and b (for brevity when doing const { a: socket1, b: socket2 } = ...)

I think it'd be OK to expose multiple names so applications can pick one that looks most relevant.


To illustrate my thought process, I think it's important to have this be a class like Duplex or PassThrough is, because I'm playing with an idea for a SimplexPair class, which is nothing more than a PassThrough with the addition of SimplexPair#readableSide and SimplexPair#writableSide properties that are only Readable and Writable, respectively.

With this, you can do patterns that maintain encapsulation, like

function generate(){
   const stream = new SimplexPair;
   stream.write("foo");
   return stream.readableSide;
}
@addaleax
Copy link
Member

  • socket1 and socket2 (what tls uses)

I like this because it matches port1 and port2 from new MessageChannel(). (Alternatively: stream1 and stream2.)

  • make the while object iterable, so you can do e.g. const [ a, b ] = DuplexPair();

Fwiw, that’s not exclusive with any naming choices. I like it. 👍

@mscdex
Copy link
Contributor

mscdex commented Jun 29, 2020

I'd prefer something even more generic than socket1 and socket2 because this interface doesn't utilize net in any way. Maybe something as simple as side1 and side2?

@awwright
Copy link
Contributor Author

awwright commented Jun 29, 2020

Check my work on the iterable here:

function DuplexPair() {
  if (!(this instanceof DuplexPair))
    return new DuplexPair();
  const clientSide = this[0] = new DuplexSide();
  const serverSide = this[1] = new DuplexSide();
  this.length = 2;
  this[Symbol.iterator] = Array.prototype[Symbol.iterator];
  clientSide[kOtherSide] = serverSide;
  serverSide[kOtherSide] = clientSide;
}

afaict, this should behave the same as an Array, and still permit iterating over the other keys in a for..in loop.

I like side1 and side2 better than anything else, except I have this hesitation because the numbers don't line up with the array indices, and I'm slightly personally attached to serverSide/clientSide, since a lot of applications will fall into this paradigm.

Do we need the named properties at all, though?

If performance with the iterator function is a concern, you can still do

const { 0: side1, 1: side2 } = new DuplexPair;

How does this sound?

@awwright awwright force-pushed the makeDuplexPair branch 6 times, most recently from 28d7814 to 892f4e0 Compare July 1, 2020 03:19
@awwright awwright requested review from mscdex and addaleax July 1, 2020 19:27
lib/tls.js Outdated Show resolved Hide resolved
lib/_stream_duplexpair.js Outdated Show resolved Hide resolved
lib/_stream_duplexpair.js Outdated Show resolved Hide resolved
@addaleax
Copy link
Member

addaleax commented Jul 7, 2020

@nodejs/streams Any further thoughts on this?

@awwright
Copy link
Contributor Author

Rebased; made a few minor changes to appease updated linter standards; and I moved the otherSide reference into a private property, which is a pattern that seems to be used elsewhere Node.js.

@awwright awwright force-pushed the makeDuplexPair branch 3 times, most recently from 11313c0 to 54b64b7 Compare November 18, 2023 18:41
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Nov 20, 2023
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Nov 20, 2023
@mcollina
Copy link
Member

The linter is failing, can you check?

@awwright
Copy link
Contributor Author

@mcollina make test passes for me and I don't have access to three of the failed tests. Is it related to the comment I dropped here?

@mcollina
Copy link
Member

@awwright you should run the linter locally with make lint. It will show you the error.

doc/api/stream.md Outdated Show resolved Hide resolved
@aduh95
Copy link
Contributor

aduh95 commented May 11, 2024

Can you please rebase and address the linter error?

awwright and others added 2 commits July 4, 2024 19:39
@awwright
Copy link
Contributor Author

awwright commented Jul 5, 2024

@aduh95 Is this good?

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 6, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 6, 2024
@awwright
Copy link
Contributor Author

I could have sworn all the tests passed when I pushed this, is that blocking the merge?

@mcollina
Copy link
Member

I could have sworn all the tests passed when I pushed this, is that blocking the merge?

Yes.

---
duration_ms: 266.324
exitcode: 1
severity: fail
stack: |2-

  node:internal/event_target:1090
    process.nextTick(() => { throw err; });
                             ^
  AssertionError [ERR_ASSERTION]: These builtins are now unexpectedly loaded at run time.
  If this is intentional, add them to `expected.atRunTime`.

  # Note: loading more builtins at run time can lead to startup performance regression.
  - Consider lazy loading builtins that are not used universally.

  --- These could be added to expected.atRunTime ---
  [
    'NativeModule internal/streams/duplexpair'
  ]

      at Object.<anonymous> (/home/iojs/build/workspace/node-test-commit-custom-suites-freestyle/test/parallel/test-bootstrap-modules.js:242:8)
      at Module._compile (node:internal/modules/cjs/loader:1458:14)
      at Object..js (node:internal/modules/cjs/loader:1542:10)
      at Module.load (node:internal/modules/cjs/loader:1282:32)
      at Function._load (node:internal/modules/cjs/loader:1098:12)
      at TracingChannel.traceSync (node:diagnostics_channel:315:14)
      at wrapModuleLoad (node:internal/modules/cjs/loader:215:24)
      at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:158:5)
      at MessagePort.<anonymous> (node:internal/main/worker_thread:181:26)
      at [nodejs.internal.kHybridDispatch] (node:internal/event_target:816:20) {
    generatedMessage: false,
    code: 'ERR_ASSERTION',
    actual: 7,
    expected: 0,
    operator: 'strictEqual'
  }

  Node.js v23.0.0-pre
...

This is relevant: https://ci.nodejs.org/job/node-test-commit-custom-suites-freestyle/36865/testReport/junit/(root)/parallel/test_bootstrap_modules/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stream Issues and PRs related to the stream subsystem.
10 participants