Skip to content

Commit e0b3afe

Browse files
authored
fix(NODE-1797): error when ChangeStream used as iterator and emitter concurrently (#2871)
1 parent 70810d1 commit e0b3afe

File tree

2 files changed

+132
-1
lines changed

2 files changed

+132
-1
lines changed

src/change_stream.ts

+26
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const kResumeQueue = Symbol('resumeQueue');
3434
const kCursorStream = Symbol('cursorStream');
3535
/** @internal */
3636
const kClosed = Symbol('closed');
37+
/** @internal */
38+
const kMode = Symbol('mode');
3739

3840
const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
3941
const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
@@ -206,6 +208,8 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
206208
[kCursorStream]?: Readable;
207209
/** @internal */
208210
[kClosed]: boolean;
211+
/** @internal */
212+
[kMode]: false | 'iterator' | 'emitter';
209213

210214
/** @event */
211215
static readonly RESPONSE = 'response' as const;
@@ -272,6 +276,7 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
272276
this.cursor = createChangeStreamCursor(this, options);
273277

274278
this[kClosed] = false;
279+
this[kMode] = false;
275280

276281
// Listen for any `change` listeners being added to ChangeStream
277282
this.on('newListener', eventName => {
@@ -299,6 +304,7 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
299304

300305
/** Check if there is any document still available in the Change Stream */
301306
hasNext(callback?: Callback): Promise<void> | void {
307+
setIsIterator(this);
302308
return maybePromise(callback, cb => {
303309
getCursor(this, (err, cursor) => {
304310
if (err || !cursor) return cb(err); // failed to resume, raise an error
@@ -313,6 +319,7 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
313319
next(
314320
callback?: Callback<ChangeStreamDocument<TSchema>>
315321
): Promise<ChangeStreamDocument<TSchema>> | void {
322+
setIsIterator(this);
316323
return maybePromise(callback, cb => {
317324
getCursor(this, (err, cursor) => {
318325
if (err || !cursor) return cb(err); // failed to resume, raise an error
@@ -367,6 +374,7 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
367374
tryNext(): Promise<Document | null>;
368375
tryNext(callback: Callback<Document | null>): void;
369376
tryNext(callback?: Callback<Document | null>): Promise<Document | null> | void {
377+
setIsIterator(this);
370378
return maybePromise(callback, cb => {
371379
getCursor(this, (err, cursor) => {
372380
if (err || !cursor) return cb(err); // failed to resume, raise an error
@@ -535,6 +543,23 @@ const CHANGE_STREAM_EVENTS = [
535543
ChangeStream.CLOSE
536544
];
537545

546+
function setIsEmitter<TSchema>(changeStream: ChangeStream<TSchema>): void {
547+
if (changeStream[kMode] === 'iterator') {
548+
throw new MongoDriverError(
549+
'Cannot use ChangeStream as an EventEmitter after using as an iterator'
550+
);
551+
}
552+
changeStream[kMode] = 'emitter';
553+
}
554+
555+
function setIsIterator<TSchema>(changeStream: ChangeStream<TSchema>): void {
556+
if (changeStream[kMode] === 'emitter') {
557+
throw new MongoDriverError(
558+
'Cannot use ChangeStream as iterator after using as an EventEmitter'
559+
);
560+
}
561+
changeStream[kMode] = 'iterator';
562+
}
538563
/**
539564
* Create a new change stream cursor based on self's configuration
540565
* @internal
@@ -630,6 +655,7 @@ function streamEvents<TSchema>(
630655
changeStream: ChangeStream<TSchema>,
631656
cursor: ChangeStreamCursor<TSchema>
632657
): void {
658+
setIsEmitter(changeStream);
633659
const stream = changeStream[kCursorStream] || cursor.stream();
634660
changeStream[kCursorStream] = stream;
635661
stream.on('data', change => processNewChange(changeStream, change));

test/functional/change_stream.test.js

+106-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22
const assert = require('assert');
33
const { Transform, PassThrough } = require('stream');
4-
const { MongoNetworkError } = require('../../src/error');
4+
const { MongoNetworkError, MongoDriverError } = require('../../src/error');
55
const { delay, setupDatabase, withClient, withCursor } = require('./shared');
66
const co = require('co');
77
const mock = require('../tools/mock');
@@ -1792,6 +1792,111 @@ describe('Change Streams', function () {
17921792
}
17931793
});
17941794

1795+
// FIXME: NODE-1797
1796+
describe('should error when used as iterator and emitter concurrently', function () {
1797+
let client, coll, changeStream, repeatInsert, val;
1798+
val = 0;
1799+
1800+
beforeEach(async function () {
1801+
client = this.configuration.newClient();
1802+
await client.connect().catch(() => expect.fail('Failed to connect to client'));
1803+
1804+
coll = client.db(this.configuration.db).collection('tester');
1805+
changeStream = coll.watch();
1806+
1807+
repeatInsert = setInterval(async function () {
1808+
await coll.insertOne({ c: val }).catch('Failed to insert document');
1809+
val++;
1810+
}, 75);
1811+
});
1812+
1813+
afterEach(async function () {
1814+
if (repeatInsert) {
1815+
clearInterval(repeatInsert);
1816+
}
1817+
if (changeStream) {
1818+
await changeStream.close();
1819+
}
1820+
1821+
await mock.cleanup();
1822+
if (client) {
1823+
await client.close();
1824+
}
1825+
});
1826+
1827+
it(
1828+
'should throw MongoDriverError when set as an emitter with "on" and used as an iterator with "hasNext"',
1829+
{
1830+
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
1831+
test: async function () {
1832+
await new Promise(resolve => changeStream.on('change', resolve));
1833+
try {
1834+
await changeStream.hasNext().catch(err => {
1835+
expect.fail(err.message);
1836+
});
1837+
} catch (error) {
1838+
return expect(error).to.be.instanceof(MongoDriverError);
1839+
}
1840+
return expect.fail('Should not reach here');
1841+
}
1842+
}
1843+
);
1844+
1845+
it(
1846+
'should throw MongoDriverError when set as an iterator with "hasNext" and used as an emitter with "on"',
1847+
{
1848+
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
1849+
test: async function () {
1850+
await changeStream
1851+
.hasNext()
1852+
.catch(() => expect.fail('Failed to set changeStream to iterator'));
1853+
try {
1854+
await new Promise(resolve => changeStream.on('change', resolve));
1855+
} catch (error) {
1856+
return expect(error).to.be.instanceof(MongoDriverError);
1857+
}
1858+
return expect.fail('Should not reach here');
1859+
}
1860+
}
1861+
);
1862+
1863+
it(
1864+
'should throw MongoDriverError when set as an emitter with "once" and used as an iterator with "next"',
1865+
{
1866+
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
1867+
test: async function () {
1868+
await new Promise(resolve => changeStream.once('change', resolve));
1869+
try {
1870+
await changeStream.next().catch(err => {
1871+
expect.fail(err.message);
1872+
});
1873+
} catch (error) {
1874+
return expect(error).to.be.instanceof(MongoDriverError);
1875+
}
1876+
return expect.fail('Should not reach here');
1877+
}
1878+
}
1879+
);
1880+
1881+
it(
1882+
'should throw MongoDriverError when set as an iterator with "tryNext" and used as an emitter with "on"',
1883+
{
1884+
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
1885+
test: async function () {
1886+
await changeStream
1887+
.tryNext()
1888+
.catch(() => expect.fail('Failed to set changeStream to iterator'));
1889+
try {
1890+
await new Promise(resolve => changeStream.on('change', resolve));
1891+
} catch (error) {
1892+
return expect(error).to.be.instanceof(MongoDriverError);
1893+
}
1894+
return expect.fail('Should not reach here');
1895+
}
1896+
}
1897+
);
1898+
});
1899+
17951900
describe('should properly handle a changeStream event being processed mid-close', function () {
17961901
let client, coll, changeStream;
17971902

0 commit comments

Comments
 (0)