change_stream.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ChangeStream = void 0;
  4. const collection_1 = require("./collection");
  5. const constants_1 = require("./constants");
  6. const abstract_cursor_1 = require("./cursor/abstract_cursor");
  7. const change_stream_cursor_1 = require("./cursor/change_stream_cursor");
  8. const db_1 = require("./db");
  9. const error_1 = require("./error");
  10. const mongo_client_1 = require("./mongo_client");
  11. const mongo_types_1 = require("./mongo_types");
  12. const resource_management_1 = require("./resource_management");
  13. const timeout_1 = require("./timeout");
  14. const utils_1 = require("./utils");
  15. const CHANGE_STREAM_OPTIONS = [
  16. 'resumeAfter',
  17. 'startAfter',
  18. 'startAtOperationTime',
  19. 'fullDocument',
  20. 'fullDocumentBeforeChange',
  21. 'showExpandedEvents'
  22. ];
  23. const CHANGE_DOMAIN_TYPES = {
  24. COLLECTION: Symbol('Collection'),
  25. DATABASE: Symbol('Database'),
  26. CLUSTER: Symbol('Cluster')
  27. };
  28. const CHANGE_STREAM_EVENTS = [constants_1.RESUME_TOKEN_CHANGED, constants_1.END, constants_1.CLOSE];
  29. const NO_RESUME_TOKEN_ERROR = 'A change stream document has been received that lacks a resume token (_id).';
  30. const CHANGESTREAM_CLOSED_ERROR = 'ChangeStream is closed';
  31. /**
  32. * Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}.
  33. * @public
  34. */
  35. class ChangeStream extends mongo_types_1.TypedEventEmitter {
  36. /** @internal */
  37. async asyncDispose() {
  38. await this.close();
  39. }
  40. /**
  41. * @internal
  42. *
  43. * @param parent - The parent object that created this change stream
  44. * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents
  45. */
  46. constructor(parent, pipeline = [], options = {}) {
  47. super();
  48. this.pipeline = pipeline;
  49. this.options = { ...options };
  50. let serverSelectionTimeoutMS;
  51. delete this.options.writeConcern;
  52. if (parent instanceof collection_1.Collection) {
  53. this.type = CHANGE_DOMAIN_TYPES.COLLECTION;
  54. serverSelectionTimeoutMS = parent.s.db.client.options.serverSelectionTimeoutMS;
  55. }
  56. else if (parent instanceof db_1.Db) {
  57. this.type = CHANGE_DOMAIN_TYPES.DATABASE;
  58. serverSelectionTimeoutMS = parent.client.options.serverSelectionTimeoutMS;
  59. }
  60. else if (parent instanceof mongo_client_1.MongoClient) {
  61. this.type = CHANGE_DOMAIN_TYPES.CLUSTER;
  62. serverSelectionTimeoutMS = parent.options.serverSelectionTimeoutMS;
  63. }
  64. else {
  65. throw new error_1.MongoChangeStreamError('Parent provided to ChangeStream constructor must be an instance of Collection, Db, or MongoClient');
  66. }
  67. this.contextOwner = Symbol();
  68. this.parent = parent;
  69. this.namespace = parent.s.namespace;
  70. if (!this.options.readPreference && parent.readPreference) {
  71. this.options.readPreference = parent.readPreference;
  72. }
  73. // Create contained Change Stream cursor
  74. this.cursor = this._createChangeStreamCursor(options);
  75. this.isClosed = false;
  76. this.mode = false;
  77. // Listen for any `change` listeners being added to ChangeStream
  78. this.on('newListener', eventName => {
  79. if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
  80. this._streamEvents(this.cursor);
  81. }
  82. });
  83. this.on('removeListener', eventName => {
  84. if (eventName === 'change' && this.listenerCount('change') === 0 && this.cursor) {
  85. this.cursorStream?.removeAllListeners('data');
  86. }
  87. });
  88. if (this.options.timeoutMS != null) {
  89. this.timeoutContext = new timeout_1.CSOTTimeoutContext({
  90. timeoutMS: this.options.timeoutMS,
  91. serverSelectionTimeoutMS
  92. });
  93. }
  94. }
  95. /** The cached resume token that is used to resume after the most recently returned change. */
  96. get resumeToken() {
  97. return this.cursor?.resumeToken;
  98. }
  99. /** Check if there is any document still available in the Change Stream */
  100. async hasNext() {
  101. this._setIsIterator();
  102. // Change streams must resume indefinitely while each resume event succeeds.
  103. // This loop continues until either a change event is received or until a resume attempt
  104. // fails.
  105. this.timeoutContext?.refresh();
  106. try {
  107. while (true) {
  108. try {
  109. const hasNext = await this.cursor.hasNext();
  110. return hasNext;
  111. }
  112. catch (error) {
  113. try {
  114. await this._processErrorIteratorMode(error, this.cursor.id != null);
  115. }
  116. catch (error) {
  117. if (error instanceof error_1.MongoOperationTimeoutError && this.cursor.id == null) {
  118. throw error;
  119. }
  120. try {
  121. await this.close();
  122. }
  123. catch (error) {
  124. (0, utils_1.squashError)(error);
  125. }
  126. throw error;
  127. }
  128. }
  129. }
  130. }
  131. finally {
  132. this.timeoutContext?.clear();
  133. }
  134. }
  135. /** Get the next available document from the Change Stream. */
  136. async next() {
  137. this._setIsIterator();
  138. // Change streams must resume indefinitely while each resume event succeeds.
  139. // This loop continues until either a change event is received or until a resume attempt
  140. // fails.
  141. this.timeoutContext?.refresh();
  142. try {
  143. while (true) {
  144. try {
  145. const change = await this.cursor.next();
  146. const processedChange = this._processChange(change ?? null);
  147. return processedChange;
  148. }
  149. catch (error) {
  150. try {
  151. await this._processErrorIteratorMode(error, this.cursor.id != null);
  152. }
  153. catch (error) {
  154. if (error instanceof error_1.MongoOperationTimeoutError && this.cursor.id == null) {
  155. throw error;
  156. }
  157. try {
  158. await this.close();
  159. }
  160. catch (error) {
  161. (0, utils_1.squashError)(error);
  162. }
  163. throw error;
  164. }
  165. }
  166. }
  167. }
  168. finally {
  169. this.timeoutContext?.clear();
  170. }
  171. }
  172. /**
  173. * Try to get the next available document from the Change Stream's cursor or `null` if an empty batch is returned
  174. */
  175. async tryNext() {
  176. this._setIsIterator();
  177. // Change streams must resume indefinitely while each resume event succeeds.
  178. // This loop continues until either a change event is received or until a resume attempt
  179. // fails.
  180. this.timeoutContext?.refresh();
  181. try {
  182. while (true) {
  183. try {
  184. const change = await this.cursor.tryNext();
  185. if (!change) {
  186. return null;
  187. }
  188. const processedChange = this._processChange(change);
  189. return processedChange;
  190. }
  191. catch (error) {
  192. try {
  193. await this._processErrorIteratorMode(error, this.cursor.id != null);
  194. }
  195. catch (error) {
  196. if (error instanceof error_1.MongoOperationTimeoutError && this.cursor.id == null)
  197. throw error;
  198. try {
  199. await this.close();
  200. }
  201. catch (error) {
  202. (0, utils_1.squashError)(error);
  203. }
  204. throw error;
  205. }
  206. }
  207. }
  208. }
  209. finally {
  210. this.timeoutContext?.clear();
  211. }
  212. }
  213. async *[Symbol.asyncIterator]() {
  214. if (this.closed) {
  215. return;
  216. }
  217. try {
  218. // Change streams run indefinitely as long as errors are resumable
  219. // So the only loop breaking condition is if `next()` throws
  220. while (true) {
  221. yield await this.next();
  222. }
  223. }
  224. finally {
  225. try {
  226. await this.close();
  227. }
  228. catch (error) {
  229. (0, utils_1.squashError)(error);
  230. }
  231. }
  232. }
  233. /** Is the cursor closed */
  234. get closed() {
  235. return this.isClosed || this.cursor.closed;
  236. }
  237. /**
  238. * Frees the internal resources used by the change stream.
  239. */
  240. async close() {
  241. this.timeoutContext?.clear();
  242. this.timeoutContext = undefined;
  243. this.isClosed = true;
  244. const cursor = this.cursor;
  245. try {
  246. await cursor.close();
  247. }
  248. finally {
  249. this._endStream();
  250. }
  251. }
  252. /**
  253. * Return a modified Readable stream including a possible transform method.
  254. *
  255. * NOTE: When using a Stream to process change stream events, the stream will
  256. * NOT automatically resume in the case a resumable error is encountered.
  257. *
  258. * @throws MongoChangeStreamError if the underlying cursor or the change stream is closed
  259. */
  260. stream(options) {
  261. if (this.closed) {
  262. throw new error_1.MongoChangeStreamError(CHANGESTREAM_CLOSED_ERROR);
  263. }
  264. this.streamOptions = options;
  265. return this.cursor.stream(options);
  266. }
  267. /** @internal */
  268. _setIsEmitter() {
  269. if (this.mode === 'iterator') {
  270. // TODO(NODE-3485): Replace with MongoChangeStreamModeError
  271. throw new error_1.MongoAPIError('ChangeStream cannot be used as an EventEmitter after being used as an iterator');
  272. }
  273. this.mode = 'emitter';
  274. }
  275. /** @internal */
  276. _setIsIterator() {
  277. if (this.mode === 'emitter') {
  278. // TODO(NODE-3485): Replace with MongoChangeStreamModeError
  279. throw new error_1.MongoAPIError('ChangeStream cannot be used as an iterator after being used as an EventEmitter');
  280. }
  281. this.mode = 'iterator';
  282. }
  283. /**
  284. * Create a new change stream cursor based on self's configuration
  285. * @internal
  286. */
  287. _createChangeStreamCursor(options) {
  288. const changeStreamStageOptions = (0, utils_1.filterOptions)(options, CHANGE_STREAM_OPTIONS);
  289. if (this.type === CHANGE_DOMAIN_TYPES.CLUSTER) {
  290. changeStreamStageOptions.allChangesForCluster = true;
  291. }
  292. const pipeline = [{ $changeStream: changeStreamStageOptions }, ...this.pipeline];
  293. const client = this.type === CHANGE_DOMAIN_TYPES.CLUSTER
  294. ? this.parent
  295. : this.type === CHANGE_DOMAIN_TYPES.DATABASE
  296. ? this.parent.client
  297. : this.type === CHANGE_DOMAIN_TYPES.COLLECTION
  298. ? this.parent.client
  299. : null;
  300. if (client == null) {
  301. // This should never happen because of the assertion in the constructor
  302. throw new error_1.MongoRuntimeError(`Changestream type should only be one of cluster, database, collection. Found ${this.type.toString()}`);
  303. }
  304. const changeStreamCursor = new change_stream_cursor_1.ChangeStreamCursor(client, this.namespace, pipeline, {
  305. ...options,
  306. timeoutContext: this.timeoutContext
  307. ? new abstract_cursor_1.CursorTimeoutContext(this.timeoutContext, this.contextOwner)
  308. : undefined
  309. });
  310. for (const event of CHANGE_STREAM_EVENTS) {
  311. changeStreamCursor.on(event, e => this.emit(event, e));
  312. }
  313. if (this.listenerCount(ChangeStream.CHANGE) > 0) {
  314. this._streamEvents(changeStreamCursor);
  315. }
  316. return changeStreamCursor;
  317. }
  318. /** @internal */
  319. _closeEmitterModeWithError(error) {
  320. this.emit(ChangeStream.ERROR, error);
  321. this.close().then(undefined, utils_1.squashError);
  322. }
  323. /** @internal */
  324. _streamEvents(cursor) {
  325. this._setIsEmitter();
  326. const stream = this.cursorStream ?? cursor.stream();
  327. this.cursorStream = stream;
  328. stream.on('data', change => {
  329. try {
  330. const processedChange = this._processChange(change);
  331. this.emit(ChangeStream.CHANGE, processedChange);
  332. }
  333. catch (error) {
  334. this.emit(ChangeStream.ERROR, error);
  335. }
  336. this.timeoutContext?.refresh();
  337. });
  338. stream.on('error', error => this._processErrorStreamMode(error, this.cursor.id != null));
  339. }
  340. /** @internal */
  341. _endStream() {
  342. this.cursorStream?.removeAllListeners('data');
  343. this.cursorStream?.removeAllListeners('close');
  344. this.cursorStream?.removeAllListeners('end');
  345. this.cursorStream?.destroy();
  346. this.cursorStream = undefined;
  347. }
  348. /** @internal */
  349. _processChange(change) {
  350. if (this.isClosed) {
  351. // TODO(NODE-3485): Replace with MongoChangeStreamClosedError
  352. throw new error_1.MongoAPIError(CHANGESTREAM_CLOSED_ERROR);
  353. }
  354. // a null change means the cursor has been notified, implicitly closing the change stream
  355. if (change == null) {
  356. // TODO(NODE-3485): Replace with MongoChangeStreamClosedError
  357. throw new error_1.MongoRuntimeError(CHANGESTREAM_CLOSED_ERROR);
  358. }
  359. if (change && !change._id) {
  360. throw new error_1.MongoChangeStreamError(NO_RESUME_TOKEN_ERROR);
  361. }
  362. // cache the resume token
  363. this.cursor.cacheResumeToken(change._id);
  364. // wipe the startAtOperationTime if there was one so that there won't be a conflict
  365. // between resumeToken and startAtOperationTime if we need to reconnect the cursor
  366. this.options.startAtOperationTime = undefined;
  367. return change;
  368. }
  369. /** @internal */
  370. _processErrorStreamMode(changeStreamError, cursorInitialized) {
  371. // If the change stream has been closed explicitly, do not process error.
  372. if (this.isClosed)
  373. return;
  374. if (cursorInitialized &&
  375. ((0, error_1.isResumableError)(changeStreamError, this.cursor.maxWireVersion) ||
  376. changeStreamError instanceof error_1.MongoOperationTimeoutError)) {
  377. this._endStream();
  378. this.cursor
  379. .close()
  380. .then(() => this._resume(changeStreamError), e => {
  381. (0, utils_1.squashError)(e);
  382. return this._resume(changeStreamError);
  383. })
  384. .then(() => {
  385. if (changeStreamError instanceof error_1.MongoOperationTimeoutError)
  386. this.emit(ChangeStream.ERROR, changeStreamError);
  387. }, () => this._closeEmitterModeWithError(changeStreamError));
  388. }
  389. else {
  390. this._closeEmitterModeWithError(changeStreamError);
  391. }
  392. }
  393. /** @internal */
  394. async _processErrorIteratorMode(changeStreamError, cursorInitialized) {
  395. if (this.isClosed) {
  396. // TODO(NODE-3485): Replace with MongoChangeStreamClosedError
  397. throw new error_1.MongoAPIError(CHANGESTREAM_CLOSED_ERROR);
  398. }
  399. if (cursorInitialized &&
  400. ((0, error_1.isResumableError)(changeStreamError, this.cursor.maxWireVersion) ||
  401. changeStreamError instanceof error_1.MongoOperationTimeoutError)) {
  402. try {
  403. await this.cursor.close();
  404. }
  405. catch (error) {
  406. (0, utils_1.squashError)(error);
  407. }
  408. await this._resume(changeStreamError);
  409. if (changeStreamError instanceof error_1.MongoOperationTimeoutError)
  410. throw changeStreamError;
  411. }
  412. else {
  413. try {
  414. await this.close();
  415. }
  416. catch (error) {
  417. (0, utils_1.squashError)(error);
  418. }
  419. throw changeStreamError;
  420. }
  421. }
  422. async _resume(changeStreamError) {
  423. this.timeoutContext?.refresh();
  424. const topology = (0, utils_1.getTopology)(this.parent);
  425. try {
  426. await topology.selectServer(this.cursor.readPreference, {
  427. operationName: 'reconnect topology in change stream',
  428. timeoutContext: this.timeoutContext
  429. });
  430. this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
  431. }
  432. catch {
  433. // if the topology can't reconnect, close the stream
  434. await this.close();
  435. throw changeStreamError;
  436. }
  437. }
  438. }
  439. exports.ChangeStream = ChangeStream;
  440. /** @event */
  441. ChangeStream.RESPONSE = constants_1.RESPONSE;
  442. /** @event */
  443. ChangeStream.MORE = constants_1.MORE;
  444. /** @event */
  445. ChangeStream.INIT = constants_1.INIT;
  446. /** @event */
  447. ChangeStream.CLOSE = constants_1.CLOSE;
  448. /**
  449. * Fired for each new matching change in the specified namespace. Attaching a `change`
  450. * event listener to a Change Stream will switch the stream into flowing mode. Data will
  451. * then be passed as soon as it is available.
  452. * @event
  453. */
  454. ChangeStream.CHANGE = constants_1.CHANGE;
  455. /** @event */
  456. ChangeStream.END = constants_1.END;
  457. /** @event */
  458. ChangeStream.ERROR = constants_1.ERROR;
  459. /**
  460. * Emitted each time the change stream stores a new resume token.
  461. * @event
  462. */
  463. ChangeStream.RESUME_TOKEN_CHANGED = constants_1.RESUME_TOKEN_CHANGED;
  464. (0, resource_management_1.configureResourceManagement)(ChangeStream.prototype);
  465. //# sourceMappingURL=change_stream.js.map