You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently Writables have a 'drain' event that fires when more writes can be done. It would be nice to have something like this for Readables when push() returns false. It's not just a matter of emitting on every call to _read() since _read() can be called when the highWaterMark hasn't been reached yet. It's especially more tricky for Transforms because you'd have to manually override _read() in your subclass, which is not fun.
Hmm, so the problem is that you want to know when a stream has started to read, whether this is the initial read or once it starts reading again after some of back pressure?
This event could be internalised without it needing to have an addition to transform streams.
What is the exact use-case that requires you to need to know these internal details?
Normally if a stream is being consumed the consumer doesn't really concern itself with anything else other than receiving a chunk of data or knowing that the source has ended.
ssh2-streams provides protocol streams where the input (responses) from the remote end comes in through the writable side and the output (requests) to the remote end goes to the readable side. The output gets pushed out via this.push(...);. The return value of push() is returned back to the user at the higher level so they know if they should continue issuing more commands or not. However the user has no way to know when they can continue issuing commands again once the readable side has dipped below the highWaterMark.
This is solved for Writable streams via the drain event when write() returns false, but no such mechanism exists when push() returns false.
I'm just trying to think if there any other use-cases where this would useful, the actually addition would be simple but i'm not sure how keen we are to add new events like this into streams. @iojs/streams any opinions?
Well, the workaround I posted does work. But I'm not sure what (if any measurable) performance overhead it may have. Also I'd rather use a standard event name if possible. continue kind of made sense to me, but it's tricky because it's really the same function as drain, but for the Readable side. readableDrain perhaps? I dunno ...
I have a custom Transform stream and I'd also like to know the proper way to handle when this.push(chunk) returns false.
Context: this Transform stream is parsing a text file formatted like{ json }{ json }{ json } and pushes each { json } chunk downstream. It appends the incoming data to an internal buffer, then parses json chunks off the internal buffer until it can't find anymore } characters.
const{ Transform }=require('stream')classMyStreamextendsTransform{constructor(opts){super(opts)// initialize an empty bufferthis._buffer=Buffer.alloc(0)}_transform(data,encoding,callback){// append all incoming data to the internal bufferthis._buffer=Buffer.concat([this._buffer,data])letfinished=falsewhile(!finished){// upstream data is ndjson with a flat structure// here's an easy way to parse each { json } chunkconststart=this._buffer.indexOf(123)// left bracket {constend=this._buffer.indexOf(125)// right bracket }// check if there are any complete { json } chunks in the internal bufferif(end<0){// no more complete { json } chunks in the internal bufferfinished=true}else{// isolate the { json } chunk in the bufferconstchunk=this._buffer.slice(start,end+1)// push the chunk downstream while capturing the backpressure flagconstbackpressure=!this.push(chunk,encoding)// remove the isolated { json } chunk from the internal bufferthis._buffer=this._buffer.slice(end+1)// handle downstream backpressureif(backpressure){// now what??? <----------------------------------------------------}}}// let upstream know we are ready for more datacallback()}}
How should I handle downstream backpressure? There still might be { json } chunks in the internal buffer, and the while loop could still need some iterations to push the data out.
Is there really some (missing) hypothetical event such as readableContinue that I need in order to properly control the backpressure flow?
Has something been implemented since this issue was opened that is the correct way of handling this situation?
Does the Transform stream handle the backpressure for me automatically here? (I can't see where)
What if my downstream Writable is really slow, or this.push(chunk) was actually sending expanded data?
Basically, any advice on this situation would be helpful. Thank you for providing your time and expertise to this project.
Transform should handle all of this for you automatically, and you should not need to worry at all. Why isn't it doing that? Can you open an new issue with a clear example of why Transform is not handling backpressure correctly?
Thank you @mcollina for the quick reply, and also thank you for making me dig deeper. Here are the questions I was originally asking.
Questions (with answers)
1. With a custom Transform stream; what happens to the chunk of data if this.push(chunk) returns false?
Answer: The chunk is pushed onto the Transform stream read buffer and is not lost. It will eventually be sent downstream and obey all backpressure rules.
2. With a custom Transform stream; what happens to the next chunks of data if this.push(chunk) returns false and I just keep calling this.push(chunk) with more data?
Answer: All chunks are pushed onto the Transform stream read buffer and are not lost. The Transform stream this.readableHighWaterMark is just a warning level of how much memory should be allocated and it will not change, even if the this.readableLength increases past the this.readableHighWaterMark. All chunks will eventually be sent downstream and obey all backpressure rules.
Simple Code Example
This is an example just trying to tease-out how the stream interface behaves. It's probably unreasonable that a transform stream would blow-out data 1000x, but who knows?
const{ Transform }=require('stream')classTransformStreamextendsTransform{_transform(chunk,encoding,callback){// 1000x data expansionleti=1000while(i--){// all of these chunks will buffer correctly and obey backpressurethis.push(chunk,encoding)// just ignore when this returns 'false' and keep pushing}callback()}}
I'm learning the API for Stream Implementers and following along with the Backpressuring in Streams guide. I'm building a custom Transform stream and it inherits from the Readable and Writable, so I thought this open issue was discussing the same problem with the Readable side of the Transform stream.
The things that misdirected me on this behavior were:
This quote. It leaves me with the question "How quickly should I stop reading from the source when .push() returns false?".
So, as well with respecting the .write() return, we must also respect the return value of .push() used in the ._read() method. If .push() returns a false value, the stream will stop reading from the source. Otherwise, it will continue without pause.
This diagram, which is confusing when you get to the Is this chunk too big? => Yes path.
The docs around the topic of .push() in the transform stream are silent about that function returning false and how to handle it.
Advanced Code Example
To see what's happening I created a Node script that prints benchmark info about each step in the pipeline as data is expanded through the transform stream, while the downstream has a delay and takes time to process.
const{ Readable, Writable, Transform }=require('stream')// random buffer with utf8: a-z dec: 97-122// NOT A CSPRNG!!! DO NOT USE FOR CRYPTO!!!constrandBuff=(size)=>{letarr=[]while(size--){arr.push(97+parseInt(Math.random()*26))}returnBuffer.from(arr)}classReadStreamextendsReadable{constructor(opts){super(opts)this.total=10}_read(){if(this.total--){console.log(`read backpressure ${!this.push(randBuff(1000))}`,`readbuffer ${this._readBuffer()}`,this.readableHighWaterMark)}else{this.push(null)}}_readBuffer(){return`${parseFloat(this.readableLength/this.readableHighWaterMark*100).toFixed(2)}%`}}classTransformStreamextendsTransform{_transform(chunk,encoding,callback){// 1000x data expansionleti=1000while(i--){console.log(`transform backpressure ${!this.push(chunk,encoding)}`,`readbuffer ${this._readBuffer()}`,this.readableHighWaterMark)}callback()}_readBuffer(){return`${parseFloat(this.readableLength/this.readableHighWaterMark*100).toFixed(2)}%`}}classWriteStreamextendsWritable{constructor(opts){super(opts)this.total=0}_write(chunk,encoding,next){setTimeout(()=>{console.log(`write #${this.total++}`,`writebuffer ${this._writeBuffer()}`,this.writableHighWaterMark)next()},1)}_writeBuffer(){return`${parseFloat(this.writableLength/this.writableHighWaterMark*100).toFixed(2)}%`}}constread=newReadStream()consttransform=newTransformStream()constwrite=newWriteStream()read.pipe(transform).pipe(write)write.on('finish',()=>{console.log('ta da!')})
Code Output
Here we can see the Transform stream calling this.push(chunk) too many times, this.readableLength grows way past the this.readableHighWaterMark, but the high water mark holds steady at 16384 (16k buffer).
Then we can watch after the Transform readable buffer is blown-out with the 1000x increase... the downstream Writable takes over and starts draining it:
After the downstream Writable is finished draining the Transform stream, it asks for more data, then the Transform stream starts the 1000x process again. This time; the Transform buffer begins entirely full (maybe I'm missing and event handler in the code example, or maybe this is normal).
Conclusion
TLDR: to implement a Transform stream correctly just keep calling this.push(chunk) until the current _transform(data, encoding, callback) data has been consumed, then call the callback(). Ignore any false return values from this.push(chunk).
The docs around this behavior could probably use some love, along with a few deeper examples on how to properly implement streams. Thanks again, and let me know if you think any of this should be re-posted somewhere else.
@alextaujenis Sorry to reply too late, but your TL;DR description of the 'correct implementation' of the Transform stream does not seem to be a 'well-behaving' one, since that kind of Transform stream will hold 1000x chunks in memory, thus make big overhead for a Node.js process, while Node.js' stream is made just to avoid such a situation. It effectively nullifies the stream's purpose.
I think a Transform stream implementation must handle false return values from this.push(chunk) just like the other readable stream implementations (and I haven't found the documentation about it yet).
@mcollina what was the final resolution was for this issue? I'm still trying to understand how I should handle when push returns false in Transform._flush().
Till there is an officially-supported solution, I've published a small package that extends the Transform class with a continue event. It also adds a Promise-based push method called safePush. It's based on the workaround first proposed by @mscdex eight years ago.
Activity
mscdex commentedon Feb 1, 2015
FWIW here's what I'm currently having to do (to avoid having to touch everywhere I do
this.push()) to get this functionality:sonewman commentedon Feb 2, 2015
Hmm, so the problem is that you want to know when a stream has started to read, whether this is the initial read or once it starts reading again after some of back pressure?
sonewman commentedon Feb 2, 2015
This event could be internalised without it needing to have an addition to transform streams.
What is the exact use-case that requires you to need to know these internal details?
Normally if a stream is being consumed the consumer doesn't really concern itself with anything else other than receiving a chunk of data or knowing that the source has ended.
mscdex commentedon Feb 2, 2015
My use-case is this:
ssh2-streamsprovides protocol streams where the input (responses) from the remote end comes in through the writable side and the output (requests) to the remote end goes to the readable side. The output gets pushed out viathis.push(...);. The return value ofpush()is returned back to the user at the higher level so they know if they should continue issuing more commands or not. However the user has no way to know when they can continue issuing commands again once the readable side has dipped below thehighWaterMark.This is solved for Writable streams via the
drainevent whenwrite()returns false, but no such mechanism exists whenpush()returns false.sonewman commentedon Feb 2, 2015
I'm just trying to think if there any other use-cases where this would useful, the actually addition would be simple but i'm not sure how keen we are to add new events like this into streams. @iojs/streams any opinions?
mscdex commentedon Feb 2, 2015
Well, the workaround I posted does work. But I'm not sure what (if any measurable) performance overhead it may have. Also I'd rather use a standard event name if possible.
continuekind of made sense to me, but it's tricky because it's really the same function asdrain, but for the Readable side.readableDrainperhaps? I dunno ...Raynos commentedon Mar 1, 2015
Nice 👍 we were talking about drain for readables.
We ended up with something like
Raynos commentedon Mar 1, 2015
@mscdex because you dont check the hwm your logic is incorrect. you could push(2mb) then read(2) and you would emit
continuetoo early.mscdex commentedon Mar 1, 2015
@Raynos You mean that
_read()can be called when the amount of buffered data is still >=highWaterMark? That doesn't sound right...Raynos commentedon Mar 1, 2015
@mscdex Oh interesting, You make a good point!
I was only using the
readableevent which can get emitted when buffered data >= highWaterMarkI think your trick is correct.
calvinmetcalf commentedon May 18, 2016
are there modules that already use the continue event? and will there be performance issues (we'd need a benchmark)
calvinmetcalf commentedon May 18, 2016
probably can't call it continue
mscdex commentedon May 18, 2016
@calvinmetcalf That's what I've been using for
ssh2/ssh2-streamsever since I created this issue.calvinmetcalf commentedon May 18, 2016
http might use that event
On Wed, May 18, 2016 at 10:34 AM Brian White notifications@github.com
wrote:
5 remaining items
mcollina commentedon May 19, 2016
@mscdex because of the missing event, i moved from your style to the "wrap" style, and not expose a Readable, but relying only on the Writable.
I think we can just do a PR and test it with the new magic flag @calvinmetcalf put in.
mcollina commentedon May 19, 2017
@mscdex this has been floating around here for some time, should we get to a PR to core? I'm happy to add it.
mscdex commentedon May 19, 2017
@mcollina I'm still all for it, but we need to come up with a suitable name I guess.
mcollina commentedon May 19, 2017
@mscdex how about
'readableContinue'? It's long and bad enough to not have any ecosystem usage.mscdex commentedon May 19, 2017
I'm not particularly picky about the name, just as long as it's clear and unique enough, so
'readableContinue'would be fine I think.alextaujenis commentedon Feb 22, 2019
Hi @mscdex and @mcollina, any progress on this?
I have a custom Transform stream and I'd also like to know the proper way to handle when
this.push(chunk)returnsfalse.Context: this Transform stream is parsing a text file formatted like
{ json }{ json }{ json }and pushes each{ json }chunk downstream. It appends the incoming data to an internal buffer, then parses json chunks off the internal buffer until it can't find anymore}characters.How should I handle downstream backpressure? There still might be
{ json }chunks in the internal buffer, and thewhileloop could still need some iterations topushthe data out.readableContinuethat I need in order to properly control the backpressure flow?this.push(chunk)was actually sending expanded data?Basically, any advice on this situation would be helpful. Thank you for providing your time and expertise to this project.
mcollina commentedon Feb 22, 2019
Transform should handle all of this for you automatically, and you should not need to worry at all. Why isn't it doing that? Can you open an new issue with a clear example of why Transform is not handling backpressure correctly?
alextaujenis commentedon Feb 22, 2019
Thank you @mcollina for the quick reply, and also thank you for making me dig deeper. Here are the questions I was originally asking.
Questions (with answers)
1. With a custom Transform stream; what happens to the
chunkof data ifthis.push(chunk)returnsfalse?Answer: The chunk is pushed onto the Transform stream read buffer and is not lost. It will eventually be sent downstream and obey all backpressure rules.
2. With a custom Transform stream; what happens to the next
chunks of data ifthis.push(chunk)returnsfalseand I just keep callingthis.push(chunk)with more data?Answer: All chunks are pushed onto the Transform stream read buffer and are not lost. The Transform stream
this.readableHighWaterMarkis just a warning level of how much memory should be allocated and it will not change, even if thethis.readableLengthincreases past thethis.readableHighWaterMark. All chunks will eventually be sent downstream and obey all backpressure rules.Simple Code Example
This is an example just trying to tease-out how the stream interface behaves. It's probably unreasonable that a transform stream would blow-out data 1000x, but who knows?
How I got here #111
I'm learning the API for Stream Implementers and following along with the Backpressuring in Streams guide. I'm building a custom Transform stream and it inherits from the Readable and Writable, so I thought this open issue was discussing the same problem with the Readable side of the Transform stream.
The things that misdirected me on this behavior were:
.push()returnsfalse?"..push()in the transform stream are silent about that function returningfalseand how to handle it.Advanced Code Example
To see what's happening I created a Node script that prints benchmark info about each step in the pipeline as data is expanded through the transform stream, while the downstream has a delay and takes time to process.
Code Output
Here we can see the Transform stream calling

this.push(chunk)too many times,this.readableLengthgrows way past thethis.readableHighWaterMark, but the high water mark holds steady at 16384 (16k buffer).Then we can watch after the Transform readable buffer is blown-out with the 1000x increase... the downstream Writable takes over and starts draining it:

After the downstream Writable is finished draining the Transform stream, it asks for more data, then the Transform stream starts the 1000x process again. This time; the Transform buffer begins entirely full (maybe I'm missing and event handler in the code example, or maybe this is normal).

Conclusion
TLDR: to implement a Transform stream correctly just keep calling
this.push(chunk)until the current_transform(data, encoding, callback)data has been consumed, then call thecallback(). Ignore anyfalsereturn values fromthis.push(chunk).The docs around this behavior could probably use some love, along with a few deeper examples on how to properly implement streams. Thanks again, and let me know if you think any of this should be re-posted somewhere else.
mcollina commentedon Feb 22, 2019
I think this is great feedback to improve our doc. Would you like to integrate https://github.com/nodejs/nodejs.org/blob/master/locale/en/docs/guides/backpressuring-in-streams.md with your finding, so it is clearer? Tag me in in the PR.
zeodtr commentedon Aug 21, 2020
@alextaujenis Sorry to reply too late, but your TL;DR description of the 'correct implementation' of the Transform stream does not seem to be a 'well-behaving' one, since that kind of Transform stream will hold 1000x chunks in memory, thus make big overhead for a Node.js process, while Node.js' stream is made just to avoid such a situation. It effectively nullifies the stream's purpose.
I think a Transform stream implementation must handle
falsereturn values fromthis.push(chunk)just like the other readable stream implementations (and I haven't found the documentation about it yet).dmurvihill commentedon Aug 24, 2023
@mcollina what was the final resolution was for this issue? I'm still trying to understand how I should handle when
pushreturns false inTransform._flush().dmurvihill commentedon Sep 3, 2023
Till there is an officially-supported solution, I've published a small package that extends the
Transformclass with acontinueevent. It also adds a Promise-basedpushmethod calledsafePush. It's based on the workaround first proposed by @mscdex eight years ago.