Skip to content

Commit abca6f1

Browse files
stream-router: support premature stream ending
1 parent 27fbcf9 commit abca6f1

File tree

3 files changed

+97
-24
lines changed

3 files changed

+97
-24
lines changed

lib/common/stream-router.js

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -84,35 +84,54 @@ streamRouter.router_ = function(args, originalMethod) {
8484
var callback = args[args.length - 1];
8585
var isStreamMode = !util.is(callback, 'function');
8686

87-
if (isStreamMode) {
88-
var stream = streamEvents(through.obj());
89-
90-
var onResultSet = function(err, results, nextQuery) {
91-
if (err) {
92-
stream.emit('error', err);
93-
stream.end();
94-
return;
95-
}
87+
if (!isStreamMode) {
88+
originalMethod.apply(null, args);
89+
return;
90+
}
9691

97-
results.forEach(function(result) {
92+
var stream = streamEvents(through.obj());
93+
94+
// Results from the API are split apart for the user. If 50 results are
95+
// returned, we emit 50 data events. While the user is consuming these, they
96+
// might choose to end the stream early by calling ".end()". We keep track of
97+
// this state to prevent pushing more results to the stream, ending it again,
98+
// or making unnecessary API calls.
99+
var streamEnded = false;
100+
var _end = stream.end;
101+
stream.end = function() {
102+
streamEnded = true;
103+
_end.apply(this, arguments);
104+
};
105+
106+
function onResultSet(err, results, nextQuery) {
107+
if (err) {
108+
stream.emit('error', err);
109+
stream.end();
110+
return;
111+
}
112+
113+
results.forEach(function(result) {
114+
if (!streamEnded) {
98115
stream.push(result);
99-
});
100-
101-
if (nextQuery) {
102-
originalMethod(nextQuery, onResultSet);
103-
} else {
104-
stream.end();
105116
}
106-
};
107-
108-
stream.once('reading', function() {
109-
originalMethod.apply(null, args.concat(onResultSet));
110117
});
111118

112-
return stream;
113-
} else {
114-
originalMethod.apply(null, args);
119+
if (streamEnded) {
120+
return;
121+
}
122+
123+
if (nextQuery) {
124+
originalMethod(nextQuery, onResultSet);
125+
} else {
126+
stream.end();
127+
}
115128
}
129+
130+
stream.once('reading', function() {
131+
originalMethod.apply(null, args.concat(onResultSet));
132+
});
133+
134+
return stream;
116135
};
117136

118137
module.exports = streamRouter;

system-test/search.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ describe('Search', function() {
146146

147147
search.getIndexes()
148148
.on('error', done)
149-
.on('data', function() { resultsMatched++; })
149+
.on('data', function() {
150+
resultsMatched++;
151+
})
150152
.on('end', function() {
151153
assert(resultsMatched > 0);
152154
done();

test/common/stream-router.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,58 @@ describe('streamRouter', function() {
208208
var rs = streamRouter.router_(ARGS_WITHOUT_CALLBACK, originalMethod);
209209
rs.on('data', util.noop); // Trigger the underlying `_read` event.
210210
});
211+
212+
it('should not push more results if stream ends early', function(done) {
213+
var results = ['a', 'b', 'c'];
214+
215+
function originalMethod() {
216+
var callback = [].slice.call(arguments).pop();
217+
setImmediate(function() {
218+
callback(null, results);
219+
});
220+
}
221+
222+
var rs = streamRouter.router_(ARGS_WITHOUT_CALLBACK, originalMethod);
223+
rs.on('data', function(result) {
224+
if (result === 'b') {
225+
// Pre-maturely end the stream.
226+
this.end();
227+
}
228+
229+
assert.notEqual(result, 'c');
230+
});
231+
rs.on('end', function() {
232+
done();
233+
});
234+
});
235+
236+
it('should not get more results if stream ends early', function(done) {
237+
var results = ['a', 'b', 'c'];
238+
239+
var originalMethodCalledCount = 0;
240+
241+
function originalMethod() {
242+
originalMethodCalledCount++;
243+
244+
var callback = [].slice.call(arguments).pop();
245+
246+
setImmediate(function() {
247+
callback(null, results, {});
248+
});
249+
}
250+
251+
var rs = streamRouter.router_(ARGS_WITHOUT_CALLBACK, originalMethod);
252+
rs.on('data', function(result) {
253+
if (result === 'b') {
254+
// Pre-maturely end the stream.
255+
this.end();
256+
}
257+
});
258+
rs.on('end', function() {
259+
assert.equal(originalMethodCalledCount, 1);
260+
done();
261+
});
262+
});
211263
});
212264

213265
describe('callback mode', function() {

0 commit comments

Comments
 (0)