Skip to content

makeDuplexPair should be the default stream paradigm #29986

Closed
@awwright

Description

@awwright

Is your feature request related to a problem? Please describe.

The Readable and Writable objects are not well encapsulated: I can inject data into a Readable stream by calling Readable#push, and I can read data from a Writable stream by monkey-patching Writable#_read.

Further, these interfaces are not symmetrical. If I want to make data available on a readable side, why not use the write API? This is how it works on every other application: If a client creates a TCP stream, there's one "writable" side, and one "readable" side; yet if I want to make both sides in the same process, I have to use a different API, for some reason.

Describe the solution you'd like

the Node.js stream library should include DuplexPair and SimplexPair objects, or equivalent factory functions.

interface SimplexPair {
   Readable readable;
   Writable writable;
}
interface DuplexPair {
   Duplex client;
   Duplex server;
}

SimplexPair

SimplexPair creates two different but related objects, one Readable and one Writable; anything written to the Writable side is made available on the Readable side. For encapsulation, the objects would not have public references to each other, and there would be no way to make data available on the readable side without access to the writable side.

DuplexPair

DuplexPair is the same, but both sides are writable and readable.

PassThrough streams

This paradigm should not be new to Node.js developers; a PassThrough stream is just a special case of SimplexPair where both sides are exposed on a single Duplex object.

Transform streams

Transform streams generate two pairs, and returns one from each:

function ROT13Pair(){
  const input = new SimplexPair;
  const output = new SimplexPair;
  input.readable.on('data', function(buf){
    output.writable.write(buf.toString().replace(/[a-zA-Z]/g, function(c){
      const d = c.charCodeAt(0) + 13;
      return String.fromCharCode( ((c<="Z")?90:122)>=d ? d : d-26 );
    }));
  });
  return {
    writable: input.writable,
    readable: output.readable,
  };
}
const { writable, readable } = new ROT13Pair;
process.stdin.pipe(writable);
readable.pipe(process.stdout);

This pattern is repeatable to any level:

function ROT26Pair(){
  const input = new ROT13Pair;
  const output = new ROT13Pair;
  input.readable.pipe(output.writable);
  return {
    writable: input.writable,
    readable: output.readable,
  };
}
const { writable, readable } = new ROT26Pair;
process.stdin.pipe(writable);
readable.pipe(process.stdout);

... although I'm not sure how practical this particular example would be in production.

I would bet this style could also result in a modest performance improvement, since much of the logic around buffering and flow control could be re-implemented.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.stalestreamIssues and PRs related to the stream subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions