mongodb 3.2.5 → 3.3.0-beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/HISTORY.md +0 -10
  2. package/index.js +4 -4
  3. package/lib/admin.js +56 -56
  4. package/lib/aggregation_cursor.js +7 -3
  5. package/lib/bulk/common.js +18 -13
  6. package/lib/change_stream.js +196 -89
  7. package/lib/collection.js +217 -169
  8. package/lib/command_cursor.js +17 -7
  9. package/lib/core/auth/auth_provider.js +158 -0
  10. package/lib/core/auth/defaultAuthProviders.js +29 -0
  11. package/lib/core/auth/gssapi.js +241 -0
  12. package/lib/core/auth/mongo_credentials.js +81 -0
  13. package/lib/core/auth/mongocr.js +51 -0
  14. package/lib/core/auth/plain.js +35 -0
  15. package/lib/core/auth/scram.js +293 -0
  16. package/lib/core/auth/sspi.js +131 -0
  17. package/lib/core/auth/x509.js +26 -0
  18. package/lib/core/connection/apm.js +236 -0
  19. package/lib/core/connection/command_result.js +36 -0
  20. package/lib/core/connection/commands.js +507 -0
  21. package/lib/core/connection/connect.js +370 -0
  22. package/lib/core/connection/connection.js +624 -0
  23. package/lib/core/connection/logger.js +246 -0
  24. package/lib/core/connection/msg.js +219 -0
  25. package/lib/core/connection/pool.js +1285 -0
  26. package/lib/core/connection/utils.js +57 -0
  27. package/lib/core/cursor.js +752 -0
  28. package/lib/core/error.js +186 -0
  29. package/lib/core/index.js +50 -0
  30. package/lib/core/sdam/monitoring.js +228 -0
  31. package/lib/core/sdam/server.js +467 -0
  32. package/lib/core/sdam/server_description.js +163 -0
  33. package/lib/core/sdam/server_selectors.js +244 -0
  34. package/lib/core/sdam/srv_polling.js +135 -0
  35. package/lib/core/sdam/topology.js +1151 -0
  36. package/lib/core/sdam/topology_description.js +408 -0
  37. package/lib/core/sessions.js +711 -0
  38. package/lib/core/tools/smoke_plugin.js +61 -0
  39. package/lib/core/topologies/mongos.js +1337 -0
  40. package/lib/core/topologies/read_preference.js +202 -0
  41. package/lib/core/topologies/replset.js +1507 -0
  42. package/lib/core/topologies/replset_state.js +1121 -0
  43. package/lib/core/topologies/server.js +984 -0
  44. package/lib/core/topologies/shared.js +453 -0
  45. package/lib/core/transactions.js +167 -0
  46. package/lib/core/uri_parser.js +631 -0
  47. package/lib/core/utils.js +165 -0
  48. package/lib/core/wireprotocol/command.js +170 -0
  49. package/lib/core/wireprotocol/compression.js +73 -0
  50. package/lib/core/wireprotocol/constants.js +13 -0
  51. package/lib/core/wireprotocol/get_more.js +86 -0
  52. package/lib/core/wireprotocol/index.js +18 -0
  53. package/lib/core/wireprotocol/kill_cursors.js +70 -0
  54. package/lib/core/wireprotocol/query.js +224 -0
  55. package/lib/core/wireprotocol/shared.js +115 -0
  56. package/lib/core/wireprotocol/write_command.js +50 -0
  57. package/lib/cursor.js +40 -46
  58. package/lib/db.js +141 -95
  59. package/lib/dynamic_loaders.js +32 -0
  60. package/lib/error.js +12 -10
  61. package/lib/gridfs/chunk.js +2 -2
  62. package/lib/gridfs/grid_store.js +31 -25
  63. package/lib/gridfs-stream/index.js +4 -4
  64. package/lib/gridfs-stream/upload.js +1 -1
  65. package/lib/mongo_client.js +37 -15
  66. package/lib/operations/add_user.js +96 -0
  67. package/lib/operations/aggregate.js +24 -13
  68. package/lib/operations/aggregate_operation.js +127 -0
  69. package/lib/operations/bulk_write.js +104 -0
  70. package/lib/operations/close.js +47 -0
  71. package/lib/operations/collection_ops.js +28 -287
  72. package/lib/operations/collections.js +55 -0
  73. package/lib/operations/command.js +120 -0
  74. package/lib/operations/command_v2.js +43 -0
  75. package/lib/operations/common_functions.js +372 -0
  76. package/lib/operations/{mongo_client_ops.js → connect.js} +185 -157
  77. package/lib/operations/count.js +72 -0
  78. package/lib/operations/count_documents.js +46 -0
  79. package/lib/operations/create_collection.js +118 -0
  80. package/lib/operations/create_index.js +92 -0
  81. package/lib/operations/create_indexes.js +61 -0
  82. package/lib/operations/cursor_ops.js +3 -4
  83. package/lib/operations/db_ops.js +15 -12
  84. package/lib/operations/delete_many.js +25 -0
  85. package/lib/operations/delete_one.js +25 -0
  86. package/lib/operations/distinct.js +85 -0
  87. package/lib/operations/drop.js +53 -0
  88. package/lib/operations/drop_index.js +42 -0
  89. package/lib/operations/drop_indexes.js +23 -0
  90. package/lib/operations/estimated_document_count.js +33 -0
  91. package/lib/operations/execute_db_admin_command.js +34 -0
  92. package/lib/operations/execute_operation.js +165 -0
  93. package/lib/operations/explain.js +23 -0
  94. package/lib/operations/find_and_modify.js +98 -0
  95. package/lib/operations/find_one.js +33 -0
  96. package/lib/operations/find_one_and_delete.js +16 -0
  97. package/lib/operations/find_one_and_replace.js +18 -0
  98. package/lib/operations/find_one_and_update.js +19 -0
  99. package/lib/operations/geo_haystack_search.js +79 -0
  100. package/lib/operations/has_next.js +40 -0
  101. package/lib/operations/index_exists.js +39 -0
  102. package/lib/operations/index_information.js +23 -0
  103. package/lib/operations/indexes.js +22 -0
  104. package/lib/operations/insert_many.js +63 -0
  105. package/lib/operations/insert_one.js +75 -0
  106. package/lib/operations/is_capped.js +19 -0
  107. package/lib/operations/list_indexes.js +66 -0
  108. package/lib/operations/map_reduce.js +189 -0
  109. package/lib/operations/next.js +32 -0
  110. package/lib/operations/operation.js +63 -0
  111. package/lib/operations/options_operation.js +32 -0
  112. package/lib/operations/profiling_level.js +31 -0
  113. package/lib/operations/re_index.js +28 -0
  114. package/lib/operations/remove_user.js +52 -0
  115. package/lib/operations/rename.js +61 -0
  116. package/lib/operations/replace_one.js +47 -0
  117. package/lib/operations/set_profiling_level.js +48 -0
  118. package/lib/operations/stats.js +45 -0
  119. package/lib/operations/to_array.js +68 -0
  120. package/lib/operations/update_many.js +29 -0
  121. package/lib/operations/update_one.js +44 -0
  122. package/lib/operations/validate_collection.js +40 -0
  123. package/lib/read_concern.js +55 -0
  124. package/lib/topologies/mongos.js +3 -3
  125. package/lib/topologies/native_topology.js +22 -2
  126. package/lib/topologies/replset.js +3 -3
  127. package/lib/topologies/server.js +4 -4
  128. package/lib/topologies/topology_base.js +6 -6
  129. package/lib/url_parser.js +4 -3
  130. package/lib/utils.js +46 -59
  131. package/lib/write_concern.js +66 -0
  132. package/package.json +15 -6
  133. package/lib/.DS_Store +0 -0
@@ -0,0 +1,711 @@
1
+ 'use strict';
2
+
3
+ const retrieveBSON = require('./connection/utils').retrieveBSON;
4
+ const EventEmitter = require('events');
5
+ const BSON = retrieveBSON();
6
+ const Binary = BSON.Binary;
7
+ const uuidV4 = require('./utils').uuidV4;
8
+ const MongoError = require('./error').MongoError;
9
+ const isRetryableError = require('././error').isRetryableError;
10
+ const MongoNetworkError = require('./error').MongoNetworkError;
11
+ const MongoWriteConcernError = require('./error').MongoWriteConcernError;
12
+ const Transaction = require('./transactions').Transaction;
13
+ const TxnState = require('./transactions').TxnState;
14
+ const isPromiseLike = require('./utils').isPromiseLike;
15
+ const ReadPreference = require('./topologies/read_preference');
16
+ const isTransactionCommand = require('./transactions').isTransactionCommand;
17
+ const resolveClusterTime = require('./topologies/shared').resolveClusterTime;
18
+ const isSharded = require('./wireprotocol/shared').isSharded;
19
+ const maxWireVersion = require('./utils').maxWireVersion;
20
+
21
+ const MAX_FOR_TRANSACTIONS = 7;
22
+
23
+ function assertAlive(session, callback) {
24
+ if (session.serverSession == null) {
25
+ const error = new MongoError('Cannot use a session that has ended');
26
+ if (typeof callback === 'function') {
27
+ callback(error, null);
28
+ return false;
29
+ }
30
+
31
+ throw error;
32
+ }
33
+
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * Options to pass when creating a Client Session
39
+ * @typedef {Object} SessionOptions
40
+ * @property {boolean} [causalConsistency=true] Whether causal consistency should be enabled on this session
41
+ * @property {TransactionOptions} [defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
42
+ */
43
+
44
+ /**
45
+ * A BSON document reflecting the lsid of a {@link ClientSession}
46
+ * @typedef {Object} SessionId
47
+ */
48
+
49
+ /**
50
+ * A class representing a client session on the server
51
+ * WARNING: not meant to be instantiated directly.
52
+ * @class
53
+ * @hideconstructor
54
+ */
55
+ class ClientSession extends EventEmitter {
56
+ /**
57
+ * Create a client session.
58
+ * WARNING: not meant to be instantiated directly
59
+ *
60
+ * @param {Topology} topology The current client's topology (Internal Class)
61
+ * @param {ServerSessionPool} sessionPool The server session pool (Internal Class)
62
+ * @param {SessionOptions} [options] Optional settings
63
+ * @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
64
+ */
65
+ constructor(topology, sessionPool, options, clientOptions) {
66
+ super();
67
+
68
+ if (topology == null) {
69
+ throw new Error('ClientSession requires a topology');
70
+ }
71
+
72
+ if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
73
+ throw new Error('ClientSession requires a ServerSessionPool');
74
+ }
75
+
76
+ options = options || {};
77
+ clientOptions = clientOptions || {};
78
+
79
+ this.topology = topology;
80
+ this.sessionPool = sessionPool;
81
+ this.hasEnded = false;
82
+ this.serverSession = sessionPool.acquire();
83
+ this.clientOptions = clientOptions;
84
+
85
+ this.supports = {
86
+ causalConsistency:
87
+ typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
88
+ };
89
+
90
+ this.clusterTime = options.initialClusterTime;
91
+
92
+ this.operationTime = null;
93
+ this.explicit = !!options.explicit;
94
+ this.owner = options.owner;
95
+ this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
96
+ this.transaction = new Transaction();
97
+ }
98
+
99
+ /**
100
+ * The server id associated with this session
101
+ * @type {SessionId}
102
+ */
103
+ get id() {
104
+ return this.serverSession.id;
105
+ }
106
+
107
+ /**
108
+ * Ends this session on the server
109
+ *
110
+ * @param {Object} [options] Optional settings. Currently reserved for future use
111
+ * @param {Function} [callback] Optional callback for completion of this operation
112
+ */
113
+ endSession(options, callback) {
114
+ if (typeof options === 'function') (callback = options), (options = {});
115
+ options = options || {};
116
+
117
+ if (this.hasEnded) {
118
+ if (typeof callback === 'function') callback(null, null);
119
+ return;
120
+ }
121
+
122
+ if (this.serverSession && this.inTransaction()) {
123
+ this.abortTransaction(); // pass in callback?
124
+ }
125
+
126
+ // mark the session as ended, and emit a signal
127
+ this.hasEnded = true;
128
+ this.emit('ended', this);
129
+
130
+ // release the server session back to the pool
131
+ this.sessionPool.release(this.serverSession);
132
+ this.serverSession = null;
133
+
134
+ // spec indicates that we should ignore all errors for `endSessions`
135
+ if (typeof callback === 'function') callback(null, null);
136
+ }
137
+
138
+ /**
139
+ * Advances the operationTime for a ClientSession.
140
+ *
141
+ * @param {Timestamp} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
142
+ */
143
+ advanceOperationTime(operationTime) {
144
+ if (this.operationTime == null) {
145
+ this.operationTime = operationTime;
146
+ return;
147
+ }
148
+
149
+ if (operationTime.greaterThan(this.operationTime)) {
150
+ this.operationTime = operationTime;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Used to determine if this session equals another
156
+ * @param {ClientSession} session
157
+ * @return {boolean} true if the sessions are equal
158
+ */
159
+ equals(session) {
160
+ if (!(session instanceof ClientSession)) {
161
+ return false;
162
+ }
163
+
164
+ return this.id.id.buffer.equals(session.id.id.buffer);
165
+ }
166
+
167
+ /**
168
+ * Increment the transaction number on the internal ServerSession
169
+ */
170
+ incrementTransactionNumber() {
171
+ this.serverSession.txnNumber++;
172
+ }
173
+
174
+ /**
175
+ * @returns {boolean} whether this session is currently in a transaction or not
176
+ */
177
+ inTransaction() {
178
+ return this.transaction.isActive;
179
+ }
180
+
181
+ /**
182
+ * Starts a new transaction with the given options.
183
+ *
184
+ * @param {TransactionOptions} options Options for the transaction
185
+ */
186
+ startTransaction(options) {
187
+ assertAlive(this);
188
+ if (this.inTransaction()) {
189
+ throw new MongoError('Transaction already in progress');
190
+ }
191
+
192
+ const topologyMaxWireVersion = maxWireVersion(this.topology);
193
+ if (
194
+ isSharded(this.topology) ||
195
+ (topologyMaxWireVersion != null && topologyMaxWireVersion < MAX_FOR_TRANSACTIONS)
196
+ ) {
197
+ throw new MongoError('Transactions are not supported on sharded clusters in MongoDB < 4.2.');
198
+ }
199
+
200
+ // increment txnNumber
201
+ this.incrementTransactionNumber();
202
+
203
+ // create transaction state
204
+ this.transaction = new Transaction(
205
+ Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
206
+ );
207
+
208
+ this.transaction.transition(TxnState.STARTING_TRANSACTION);
209
+ }
210
+
211
+ /**
212
+ * Commits the currently active transaction in this session.
213
+ *
214
+ * @param {Function} [callback] optional callback for completion of this operation
215
+ * @return {Promise} A promise is returned if no callback is provided
216
+ */
217
+ commitTransaction(callback) {
218
+ if (typeof callback === 'function') {
219
+ endTransaction(this, 'commitTransaction', callback);
220
+ return;
221
+ }
222
+
223
+ return new Promise((resolve, reject) => {
224
+ endTransaction(
225
+ this,
226
+ 'commitTransaction',
227
+ (err, reply) => (err ? reject(err) : resolve(reply))
228
+ );
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Aborts the currently active transaction in this session.
234
+ *
235
+ * @param {Function} [callback] optional callback for completion of this operation
236
+ * @return {Promise} A promise is returned if no callback is provided
237
+ */
238
+ abortTransaction(callback) {
239
+ if (typeof callback === 'function') {
240
+ endTransaction(this, 'abortTransaction', callback);
241
+ return;
242
+ }
243
+
244
+ return new Promise((resolve, reject) => {
245
+ endTransaction(
246
+ this,
247
+ 'abortTransaction',
248
+ (err, reply) => (err ? reject(err) : resolve(reply))
249
+ );
250
+ });
251
+ }
252
+
253
+ /**
254
+ * This is here to ensure that ClientSession is never serialized to BSON.
255
+ * @ignore
256
+ */
257
+ toBSON() {
258
+ throw new Error('ClientSession cannot be serialized to BSON.');
259
+ }
260
+
261
+ /**
262
+ * A user provided function to be run within a transaction
263
+ *
264
+ * @callback WithTransactionCallback
265
+ * @param {ClientSession} session The parent session of the transaction running the operation. This should be passed into each operation within the lambda.
266
+ * @returns {Promise} The resulting Promise of operations run within this transaction
267
+ */
268
+
269
+ /**
270
+ * Runs a provided lambda within a transaction, retrying either the commit operation
271
+ * or entire transaction as needed (and when the error permits) to better ensure that
272
+ * the transaction can complete successfully.
273
+ *
274
+ * IMPORTANT: This method requires the user to return a Promise, all lambdas that do not
275
+ * return a Promise will result in undefined behavior.
276
+ *
277
+ * @param {WithTransactionCallback} fn
278
+ * @param {TransactionOptions} [options] Optional settings for the transaction
279
+ */
280
+ withTransaction(fn, options) {
281
+ const startTime = Date.now();
282
+ return attemptTransaction(this, startTime, fn, options);
283
+ }
284
+ }
285
+
286
+ const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
287
+ const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
288
+ const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
289
+ const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
290
+ 'CannotSatisfyWriteConcern',
291
+ 'UnknownReplWriteConcern',
292
+ 'UnsatisfiableWriteConcern'
293
+ ]);
294
+
295
+ function hasNotTimedOut(startTime, max) {
296
+ return Date.now() - startTime < max;
297
+ }
298
+
299
+ function isUnknownTransactionCommitResult(err) {
300
+ return (
301
+ !NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
302
+ err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
303
+ err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
304
+ );
305
+ }
306
+
307
+ function attemptTransactionCommit(session, startTime, fn, options) {
308
+ return session.commitTransaction().catch(err => {
309
+ if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
310
+ if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
311
+ return attemptTransactionCommit(session, startTime, fn, options);
312
+ }
313
+
314
+ if (err.hasErrorLabel('TransientTransactionError')) {
315
+ return attemptTransaction(session, startTime, fn, options);
316
+ }
317
+ }
318
+
319
+ throw err;
320
+ });
321
+ }
322
+
323
+ const USER_EXPLICIT_TXN_END_STATES = new Set([
324
+ TxnState.NO_TRANSACTION,
325
+ TxnState.TRANSACTION_COMMITTED,
326
+ TxnState.TRANSACTION_ABORTED
327
+ ]);
328
+
329
+ function userExplicitlyEndedTransaction(session) {
330
+ return USER_EXPLICIT_TXN_END_STATES.has(session.transaction.state);
331
+ }
332
+
333
+ function attemptTransaction(session, startTime, fn, options) {
334
+ session.startTransaction(options);
335
+
336
+ let promise;
337
+ try {
338
+ promise = fn(session);
339
+ } catch (err) {
340
+ promise = Promise.reject(err);
341
+ }
342
+
343
+ if (!isPromiseLike(promise)) {
344
+ session.abortTransaction();
345
+ throw new TypeError('Function provided to `withTransaction` must return a Promise');
346
+ }
347
+
348
+ return promise
349
+ .then(() => {
350
+ if (userExplicitlyEndedTransaction(session)) {
351
+ return;
352
+ }
353
+
354
+ return attemptTransactionCommit(session, startTime, fn, options);
355
+ })
356
+ .catch(err => {
357
+ function maybeRetryOrThrow(err) {
358
+ if (
359
+ err instanceof MongoError &&
360
+ err.hasErrorLabel('TransientTransactionError') &&
361
+ hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
362
+ ) {
363
+ return attemptTransaction(session, startTime, fn, options);
364
+ }
365
+
366
+ throw err;
367
+ }
368
+
369
+ if (session.transaction.isActive) {
370
+ return session.abortTransaction().then(() => maybeRetryOrThrow(err));
371
+ }
372
+
373
+ return maybeRetryOrThrow(err);
374
+ });
375
+ }
376
+
377
+ function endTransaction(session, commandName, callback) {
378
+ if (!assertAlive(session, callback)) {
379
+ // checking result in case callback was called
380
+ return;
381
+ }
382
+
383
+ // handle any initial problematic cases
384
+ let txnState = session.transaction.state;
385
+
386
+ if (txnState === TxnState.NO_TRANSACTION) {
387
+ callback(new MongoError('No transaction started'));
388
+ return;
389
+ }
390
+
391
+ if (commandName === 'commitTransaction') {
392
+ if (
393
+ txnState === TxnState.STARTING_TRANSACTION ||
394
+ txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
395
+ ) {
396
+ // the transaction was never started, we can safely exit here
397
+ session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
398
+ callback(null, null);
399
+ return;
400
+ }
401
+
402
+ if (txnState === TxnState.TRANSACTION_ABORTED) {
403
+ callback(new MongoError('Cannot call commitTransaction after calling abortTransaction'));
404
+ return;
405
+ }
406
+ } else {
407
+ if (txnState === TxnState.STARTING_TRANSACTION) {
408
+ // the transaction was never started, we can safely exit here
409
+ session.transaction.transition(TxnState.TRANSACTION_ABORTED);
410
+ callback(null, null);
411
+ return;
412
+ }
413
+
414
+ if (txnState === TxnState.TRANSACTION_ABORTED) {
415
+ callback(new MongoError('Cannot call abortTransaction twice'));
416
+ return;
417
+ }
418
+
419
+ if (
420
+ txnState === TxnState.TRANSACTION_COMMITTED ||
421
+ txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
422
+ ) {
423
+ callback(new MongoError('Cannot call abortTransaction after calling commitTransaction'));
424
+ return;
425
+ }
426
+ }
427
+
428
+ // construct and send the command
429
+ const command = { [commandName]: 1 };
430
+
431
+ // apply a writeConcern if specified
432
+ let writeConcern;
433
+ if (session.transaction.options.writeConcern) {
434
+ writeConcern = Object.assign({}, session.transaction.options.writeConcern);
435
+ } else if (session.clientOptions && session.clientOptions.w) {
436
+ writeConcern = { w: session.clientOptions.w };
437
+ }
438
+
439
+ if (txnState === TxnState.TRANSACTION_COMMITTED) {
440
+ writeConcern = Object.assign({ wtimeout: 10000 }, writeConcern, { w: 'majority' });
441
+ }
442
+
443
+ if (writeConcern) {
444
+ Object.assign(command, { writeConcern });
445
+ }
446
+
447
+ function commandHandler(e, r) {
448
+ if (commandName === 'commitTransaction') {
449
+ session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
450
+
451
+ if (
452
+ e &&
453
+ (e instanceof MongoNetworkError ||
454
+ e instanceof MongoWriteConcernError ||
455
+ isRetryableError(e))
456
+ ) {
457
+ if (e.errorLabels) {
458
+ const idx = e.errorLabels.indexOf('TransientTransactionError');
459
+ if (idx !== -1) {
460
+ e.errorLabels.splice(idx, 1);
461
+ }
462
+ } else {
463
+ e.errorLabels = [];
464
+ }
465
+
466
+ if (isUnknownTransactionCommitResult(e)) {
467
+ e.errorLabels.push('UnknownTransactionCommitResult');
468
+
469
+ // per txns spec, must unpin session in this case
470
+ session.transaction.unpinServer();
471
+ }
472
+ }
473
+ } else {
474
+ session.transaction.transition(TxnState.TRANSACTION_ABORTED);
475
+ }
476
+
477
+ callback(e, r);
478
+ }
479
+
480
+ // The spec indicates that we should ignore all errors on `abortTransaction`
481
+ function transactionError(err) {
482
+ return commandName === 'commitTransaction' ? err : null;
483
+ }
484
+
485
+ if (
486
+ // Assumption here that commandName is "commitTransaction" or "abortTransaction"
487
+ session.transaction.recoveryToken &&
488
+ supportsRecoveryToken(session)
489
+ ) {
490
+ command.recoveryToken = session.transaction.recoveryToken;
491
+ }
492
+
493
+ // send the command
494
+ session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
495
+ if (err && isRetryableError(err)) {
496
+ // SPEC-1185: apply majority write concern when retrying commitTransaction
497
+ if (command.commitTransaction) {
498
+ // per txns spec, must unpin session in this case
499
+ session.transaction.unpinServer();
500
+
501
+ command.writeConcern = Object.assign({ wtimeout: 10000 }, command.writeConcern, {
502
+ w: 'majority'
503
+ });
504
+ }
505
+
506
+ return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
507
+ commandHandler(transactionError(_err), _reply)
508
+ );
509
+ }
510
+
511
+ commandHandler(transactionError(err), reply);
512
+ });
513
+ }
514
+
515
+ function supportsRecoveryToken(session) {
516
+ const topology = session.topology;
517
+ return !!topology.s.options.useRecoveryToken;
518
+ }
519
+
520
+ /**
521
+ * Reflects the existence of a session on the server. Can be reused by the session pool.
522
+ * WARNING: not meant to be instantiated directly. For internal use only.
523
+ * @ignore
524
+ */
525
+ class ServerSession {
526
+ constructor() {
527
+ this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
528
+ this.lastUse = Date.now();
529
+ this.txnNumber = 0;
530
+ }
531
+
532
+ /**
533
+ * Determines if the server session has timed out.
534
+ * @ignore
535
+ * @param {Date} sessionTimeoutMinutes The server's "logicalSessionTimeoutMinutes"
536
+ * @return {boolean} true if the session has timed out.
537
+ */
538
+ hasTimedOut(sessionTimeoutMinutes) {
539
+ // Take the difference of the lastUse timestamp and now, which will result in a value in
540
+ // milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
541
+ const idleTimeMinutes = Math.round(
542
+ (((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
543
+ );
544
+
545
+ return idleTimeMinutes > sessionTimeoutMinutes - 1;
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Maintains a pool of Server Sessions.
551
+ * For internal use only
552
+ * @ignore
553
+ */
554
+ class ServerSessionPool {
555
+ constructor(topology) {
556
+ if (topology == null) {
557
+ throw new Error('ServerSessionPool requires a topology');
558
+ }
559
+
560
+ this.topology = topology;
561
+ this.sessions = [];
562
+ }
563
+
564
+ /**
565
+ * Ends all sessions in the session pool.
566
+ * @ignore
567
+ */
568
+ endAllPooledSessions() {
569
+ if (this.sessions.length) {
570
+ this.topology.endSessions(this.sessions.map(session => session.id));
571
+ this.sessions = [];
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Acquire a Server Session from the pool.
577
+ * Iterates through each session in the pool, removing any stale sessions
578
+ * along the way. The first non-stale session found is removed from the
579
+ * pool and returned. If no non-stale session is found, a new ServerSession
580
+ * is created.
581
+ * @ignore
582
+ * @returns {ServerSession}
583
+ */
584
+ acquire() {
585
+ const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
586
+ while (this.sessions.length) {
587
+ const session = this.sessions.shift();
588
+ if (!session.hasTimedOut(sessionTimeoutMinutes)) {
589
+ return session;
590
+ }
591
+ }
592
+
593
+ return new ServerSession();
594
+ }
595
+
596
+ /**
597
+ * Release a session to the session pool
598
+ * Adds the session back to the session pool if the session has not timed out yet.
599
+ * This method also removes any stale sessions from the pool.
600
+ * @ignore
601
+ * @param {ServerSession} session The session to release to the pool
602
+ */
603
+ release(session) {
604
+ const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
605
+ while (this.sessions.length) {
606
+ const session = this.sessions[this.sessions.length - 1];
607
+ if (session.hasTimedOut(sessionTimeoutMinutes)) {
608
+ this.sessions.pop();
609
+ } else {
610
+ break;
611
+ }
612
+ }
613
+
614
+ if (!session.hasTimedOut(sessionTimeoutMinutes)) {
615
+ this.sessions.unshift(session);
616
+ }
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Optionally decorate a command with sessions specific keys
622
+ *
623
+ * @param {ClientSession} session the session tracking transaction state
624
+ * @param {Object} command the command to decorate
625
+ * @param {Object} topology the topology for tracking the cluster time
626
+ * @param {Object} [options] Optional settings passed to calling operation
627
+ * @return {MongoError|null} An error, if some error condition was met
628
+ */
629
+ function applySession(session, command, options) {
630
+ const serverSession = session.serverSession;
631
+ if (serverSession == null) {
632
+ // TODO: merge this with `assertAlive`, did not want to throw a try/catch here
633
+ return new MongoError('Cannot use a session that has ended');
634
+ }
635
+
636
+ // mark the last use of this session, and apply the `lsid`
637
+ serverSession.lastUse = Date.now();
638
+ command.lsid = serverSession.id;
639
+
640
+ // first apply non-transaction-specific sessions data
641
+ const inTransaction = session.inTransaction() || isTransactionCommand(command);
642
+ const isRetryableWrite = options.willRetryWrite;
643
+
644
+ if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
645
+ command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
646
+ }
647
+
648
+ // now attempt to apply transaction-specific sessions data
649
+ if (!inTransaction) {
650
+ if (session.transaction.state !== TxnState.NO_TRANSACTION) {
651
+ session.transaction.transition(TxnState.NO_TRANSACTION);
652
+ }
653
+
654
+ // TODO: the following should only be applied to read operation per spec.
655
+ // for causal consistency
656
+ if (session.supports.causalConsistency && session.operationTime) {
657
+ command.readConcern = command.readConcern || {};
658
+ Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
659
+ }
660
+
661
+ return;
662
+ }
663
+
664
+ if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
665
+ return new MongoError(
666
+ `Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
667
+ );
668
+ }
669
+
670
+ // `autocommit` must always be false to differentiate from retryable writes
671
+ command.autocommit = false;
672
+
673
+ if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
674
+ session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
675
+ command.startTransaction = true;
676
+
677
+ const readConcern =
678
+ session.transaction.options.readConcern || session.clientOptions.readConcern;
679
+ if (readConcern) {
680
+ command.readConcern = readConcern;
681
+ }
682
+
683
+ if (session.supports.causalConsistency && session.operationTime) {
684
+ command.readConcern = command.readConcern || {};
685
+ Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
686
+ }
687
+ }
688
+ }
689
+
690
+ function updateSessionFromResponse(session, document) {
691
+ if (document.$clusterTime) {
692
+ resolveClusterTime(session, document.$clusterTime);
693
+ }
694
+
695
+ if (document.operationTime && session && session.supports.causalConsistency) {
696
+ session.advanceOperationTime(document.operationTime);
697
+ }
698
+
699
+ if (document.recoveryToken && session && session.inTransaction()) {
700
+ session.transaction._recoveryToken = document.recoveryToken;
701
+ }
702
+ }
703
+
704
+ module.exports = {
705
+ ClientSession,
706
+ ServerSession,
707
+ ServerSessionPool,
708
+ TxnState,
709
+ applySession,
710
+ updateSessionFromResponse
711
+ };