Skip to content

Commit cf5f986

Browse files
committed
stream: 'readable' have precedence over flowing
In Streams3 the 'readable' event/.read() method had a lower precedence than the `'data'` event that made them impossible to use them together. This make `.resume()` a no-op if there is a listener for the `'readable'` event, making the stream non-flowing if there is a `'data'`  listener. Fixes: #18058 PR-URL: #18994 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent 1e07acd commit cf5f986

5 files changed

+269
-49
lines changed

doc/api/stream.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,9 @@ changes:
762762
description: >
763763
'readable' is always emitted in the next tick after
764764
.push() is called
765+
- version: REPLACEME
766+
pr-url: https://github.com/nodejs/node/pull/18994
767+
description: Using 'readable' requires calling .read().
765768
-->
766769

767770
The `'readable'` event is emitted when there is data available to be read from
@@ -770,10 +773,16 @@ cause some amount of data to be read into an internal buffer.
770773

771774
```javascript
772775
const readable = getReadableStreamSomehow();
773-
readable.on('readable', () => {
776+
readable.on('readable', function() {
774777
// there is some data to read now
778+
let data;
779+
780+
while (data = this.read()) {
781+
console.log(data);
782+
}
775783
});
776784
```
785+
777786
The `'readable'` event will also be emitted once the end of the stream data
778787
has been reached but before the `'end'` event is emitted.
779788

@@ -806,6 +815,10 @@ In general, the `readable.pipe()` and `'data'` event mechanisms are easier to
806815
understand than the `'readable'` event. However, handling `'readable'` might
807816
result in increased throughput.
808817

818+
If both `'readable'` and [`'data'`][] are used at the same time, `'readable'`
819+
takes precedence in controlling the flow, i.e. `'data'` will be emitted
820+
only when [`stream.read()`][stream-read] is called.
821+
809822
##### readable.destroy([error])
810823
<!-- YAML
811824
added: v8.0.0
@@ -997,6 +1010,10 @@ the status of the `highWaterMark`.
9971010
##### readable.resume()
9981011
<!-- YAML
9991012
added: v0.9.4
1013+
changes:
1014+
- version: REPLACEME
1015+
pr-url: https://github.com/nodejs/node/pull/18994
1016+
description: Resume has no effect if there is a 'readable' event listening
10001017
-->
10011018

10021019
* Returns: {this}
@@ -1016,6 +1033,9 @@ getReadableStreamSomehow()
10161033
});
10171034
```
10181035

1036+
The `readable.resume()` method has no effect if there is a `'readable'`
1037+
event listener.
1038+
10191039
##### readable.setEncoding(encoding)
10201040
<!-- YAML
10211041
added: v0.9.4

lib/_stream_readable.js

+50-7
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ Readable.prototype.unshift = function(chunk) {
223223
};
224224

225225
function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) {
226+
debug('readableAddChunk', chunk);
226227
var state = stream._readableState;
227228
if (chunk === null) {
228229
state.reading = false;
@@ -799,20 +800,24 @@ Readable.prototype.unpipe = function(dest) {
799800
// Ensure readable listeners eventually get something
800801
Readable.prototype.on = function(ev, fn) {
801802
const res = Stream.prototype.on.call(this, ev, fn);
803+
const state = this._readableState;
802804

803805
if (ev === 'data') {
804-
// Start flowing on next tick if stream isn't explicitly paused
805-
if (this._readableState.flowing !== false)
806+
// update readableListening so that resume() may be a no-op
807+
// a few lines down. This is needed to support once('readable').
808+
state.readableListening = this.listenerCount('readable') > 0;
809+
810+
// Try start flowing on next tick if stream isn't explicitly paused
811+
if (state.flowing !== false)
806812
this.resume();
807813
} else if (ev === 'readable') {
808-
const state = this._readableState;
809814
if (!state.endEmitted && !state.readableListening) {
810815
state.readableListening = state.needReadable = true;
811816
state.emittedReadable = false;
812-
if (!state.reading) {
813-
process.nextTick(nReadingNextTick, this);
814-
} else if (state.length) {
817+
if (state.length) {
815818
emitReadable(this);
819+
} else if (!state.reading) {
820+
process.nextTick(nReadingNextTick, this);
816821
}
817822
}
818823
}
@@ -821,6 +826,42 @@ Readable.prototype.on = function(ev, fn) {
821826
};
822827
Readable.prototype.addListener = Readable.prototype.on;
823828

829+
Readable.prototype.removeListener = function(ev, fn) {
830+
const res = Stream.prototype.removeListener.call(this, ev, fn);
831+
832+
if (ev === 'readable') {
833+
// We need to check if there is someone still listening to
834+
// to readable and reset the state. However this needs to happen
835+
// after readable has been emitted but before I/O (nextTick) to
836+
// support once('readable', fn) cycles. This means that calling
837+
// resume within the same tick will have no
838+
// effect.
839+
process.nextTick(updateReadableListening, this);
840+
}
841+
842+
return res;
843+
};
844+
845+
Readable.prototype.removeAllListeners = function(ev) {
846+
const res = Stream.prototype.removeAllListeners.call(this, ev);
847+
848+
if (ev === 'readable' || ev === undefined) {
849+
// We need to check if there is someone still listening to
850+
// to readable and reset the state. However this needs to happen
851+
// after readable has been emitted but before I/O (nextTick) to
852+
// support once('readable', fn) cycles. This means that calling
853+
// resume within the same tick will have no
854+
// effect.
855+
process.nextTick(updateReadableListening, this);
856+
}
857+
858+
return res;
859+
};
860+
861+
function updateReadableListening(self) {
862+
self._readableState.readableListening = self.listenerCount('readable') > 0;
863+
}
864+
824865
function nReadingNextTick(self) {
825866
debug('readable nexttick read 0');
826867
self.read(0);
@@ -832,7 +873,9 @@ Readable.prototype.resume = function() {
832873
var state = this._readableState;
833874
if (!state.flowing) {
834875
debug('resume');
835-
state.flowing = true;
876+
// we flow only if there is no one listening
877+
// for readable
878+
state.flowing = !state.readableListening;
836879
resume(this, state);
837880
}
838881
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const helloWorld = 'Hello World!';
7+
const helloAgainLater = 'Hello again later!';
8+
9+
const server = http.createServer((req, res) => {
10+
res.writeHead(200, {
11+
'Content-Length': '' + (helloWorld.length + helloAgainLater.length)
12+
});
13+
res.write(helloWorld);
14+
15+
// we need to make sure the data is flushed
16+
setTimeout(() => {
17+
res.end(helloAgainLater);
18+
}, common.platformTimeout(10));
19+
}).listen(0, function() {
20+
const opts = {
21+
hostname: 'localhost',
22+
port: server.address().port,
23+
path: '/'
24+
};
25+
26+
const expectedData = [helloWorld, helloAgainLater];
27+
const expectedRead = [helloWorld, null, helloAgainLater, null];
28+
29+
const req = http.request(opts, (res) => {
30+
res.on('error', common.mustNotCall);
31+
32+
res.on('readable', common.mustCall(() => {
33+
let data;
34+
35+
do {
36+
data = res.read();
37+
assert.strictEqual(data, expectedRead.shift());
38+
} while (data !== null);
39+
}, 2));
40+
41+
res.setEncoding('utf8');
42+
res.on('data', common.mustCall((data) => {
43+
assert.strictEqual(data, expectedData.shift());
44+
}, 2));
45+
46+
res.on('end', common.mustCall(() => {
47+
server.close();
48+
}));
49+
});
50+
51+
req.end();
52+
});

0 commit comments

Comments
 (0)