mongodb-livedata-server 0.0.4 → 0.0.6

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 (93) hide show
  1. package/{livedata_server.ts → dist/livedata_server.d.ts} +1 -1
  2. package/dist/livedata_server.js +3 -1
  3. package/dist/meteor/binary-heap/max_heap.d.ts +31 -0
  4. package/dist/meteor/binary-heap/min_heap.d.ts +6 -0
  5. package/dist/meteor/binary-heap/min_max_heap.d.ts +11 -0
  6. package/dist/meteor/callback-hook/hook.d.ts +11 -0
  7. package/dist/meteor/ddp/crossbar.d.ts +15 -0
  8. package/dist/meteor/ddp/heartbeat.d.ts +19 -0
  9. package/dist/meteor/ddp/livedata_server.d.ts +141 -0
  10. package/dist/meteor/ddp/method-invocation.d.ts +25 -0
  11. package/dist/meteor/ddp/random-stream.d.ts +8 -0
  12. package/dist/meteor/ddp/session-collection-view.d.ts +27 -0
  13. package/dist/meteor/ddp/session-document-view.d.ts +8 -0
  14. package/dist/meteor/ddp/session.d.ts +69 -0
  15. package/dist/meteor/ddp/stream_server.d.ts +21 -0
  16. package/dist/meteor/ddp/subscription.d.ts +89 -0
  17. package/dist/meteor/ddp/utils.d.ts +8 -0
  18. package/dist/meteor/ddp/writefence.d.ts +20 -0
  19. package/dist/meteor/diff-sequence/diff.d.ts +13 -0
  20. package/dist/meteor/ejson/ejson.d.ts +82 -0
  21. package/dist/meteor/ejson/stringify.d.ts +2 -0
  22. package/dist/meteor/ejson/utils.d.ts +12 -0
  23. package/dist/meteor/id-map/id_map.d.ts +16 -0
  24. package/dist/meteor/mongo/caching_change_observer.d.ts +16 -0
  25. package/dist/meteor/mongo/doc_fetcher.d.ts +7 -0
  26. package/dist/meteor/mongo/geojson_utils.d.ts +3 -0
  27. package/dist/meteor/mongo/live_connection.d.ts +27 -0
  28. package/dist/meteor/mongo/live_cursor.d.ts +25 -0
  29. package/dist/meteor/mongo/minimongo_common.d.ts +84 -0
  30. package/dist/meteor/mongo/minimongo_matcher.d.ts +22 -0
  31. package/dist/meteor/mongo/minimongo_sorter.d.ts +16 -0
  32. package/dist/meteor/mongo/observe_driver_utils.d.ts +9 -0
  33. package/dist/meteor/mongo/observe_multiplexer.d.ts +36 -0
  34. package/dist/meteor/mongo/oplog-observe-driver.d.ts +67 -0
  35. package/dist/meteor/mongo/oplog_tailing.d.ts +35 -0
  36. package/dist/meteor/mongo/oplog_v2_converter.d.ts +1 -0
  37. package/dist/meteor/mongo/polling_observe_driver.d.ts +30 -0
  38. package/dist/meteor/mongo/synchronous-cursor.d.ts +17 -0
  39. package/dist/meteor/mongo/synchronous-queue.d.ts +14 -0
  40. package/dist/meteor/ordered-dict/ordered_dict.d.ts +31 -0
  41. package/dist/meteor/random/AbstractRandomGenerator.d.ts +42 -0
  42. package/dist/meteor/random/AleaRandomGenerator.d.ts +13 -0
  43. package/dist/meteor/random/NodeRandomGenerator.d.ts +16 -0
  44. package/dist/meteor/random/createAleaGenerator.d.ts +2 -0
  45. package/dist/meteor/random/createRandom.d.ts +1 -0
  46. package/dist/meteor/random/main.d.ts +1 -0
  47. package/package.json +2 -2
  48. package/meteor/LICENSE +0 -28
  49. package/meteor/binary-heap/max_heap.ts +0 -225
  50. package/meteor/binary-heap/min_heap.ts +0 -15
  51. package/meteor/binary-heap/min_max_heap.ts +0 -53
  52. package/meteor/callback-hook/hook.ts +0 -85
  53. package/meteor/ddp/crossbar.ts +0 -148
  54. package/meteor/ddp/heartbeat.ts +0 -97
  55. package/meteor/ddp/livedata_server.ts +0 -474
  56. package/meteor/ddp/method-invocation.ts +0 -86
  57. package/meteor/ddp/random-stream.ts +0 -102
  58. package/meteor/ddp/session-collection-view.ts +0 -119
  59. package/meteor/ddp/session-document-view.ts +0 -92
  60. package/meteor/ddp/session.ts +0 -708
  61. package/meteor/ddp/stream_server.ts +0 -204
  62. package/meteor/ddp/subscription.ts +0 -392
  63. package/meteor/ddp/utils.ts +0 -119
  64. package/meteor/ddp/writefence.ts +0 -130
  65. package/meteor/diff-sequence/diff.ts +0 -295
  66. package/meteor/ejson/ejson.ts +0 -601
  67. package/meteor/ejson/stringify.ts +0 -122
  68. package/meteor/ejson/utils.ts +0 -38
  69. package/meteor/id-map/id_map.ts +0 -84
  70. package/meteor/mongo/caching_change_observer.ts +0 -120
  71. package/meteor/mongo/doc_fetcher.ts +0 -52
  72. package/meteor/mongo/geojson_utils.ts +0 -42
  73. package/meteor/mongo/live_connection.ts +0 -302
  74. package/meteor/mongo/live_cursor.ts +0 -79
  75. package/meteor/mongo/minimongo_common.ts +0 -2440
  76. package/meteor/mongo/minimongo_matcher.ts +0 -275
  77. package/meteor/mongo/minimongo_sorter.ts +0 -331
  78. package/meteor/mongo/observe_driver_utils.ts +0 -79
  79. package/meteor/mongo/observe_multiplexer.ts +0 -256
  80. package/meteor/mongo/oplog-observe-driver.ts +0 -1049
  81. package/meteor/mongo/oplog_tailing.ts +0 -414
  82. package/meteor/mongo/oplog_v2_converter.ts +0 -124
  83. package/meteor/mongo/polling_observe_driver.ts +0 -247
  84. package/meteor/mongo/synchronous-cursor.ts +0 -293
  85. package/meteor/mongo/synchronous-queue.ts +0 -119
  86. package/meteor/ordered-dict/ordered_dict.ts +0 -229
  87. package/meteor/random/AbstractRandomGenerator.ts +0 -99
  88. package/meteor/random/AleaRandomGenerator.ts +0 -96
  89. package/meteor/random/NodeRandomGenerator.ts +0 -37
  90. package/meteor/random/createAleaGenerator.ts +0 -31
  91. package/meteor/random/createRandom.ts +0 -19
  92. package/meteor/random/main.ts +0 -8
  93. package/tsconfig.json +0 -10
@@ -1,414 +0,0 @@
1
- import { Filter, Long, Timestamp } from "mongodb";
2
- import { Hook } from "../callback-hook/hook";
3
- import { _Crossbar } from "../ddp/crossbar";
4
- import DoubleEndedQueue from "double-ended-queue";
5
- import { stringify } from "../ejson/ejson";
6
- import { LiveMongoConnection } from "./live_connection";
7
- import { CursorDescription } from "./live_cursor";
8
-
9
- export const OPLOG_COLLECTION = 'oplog.rs';
10
-
11
- var TOO_FAR_BEHIND = process.env.METEOR_OPLOG_TOO_FAR_BEHIND || 2000;
12
- var TAIL_TIMEOUT = +process.env.METEOR_OPLOG_TAIL_TIMEOUT || 30000;
13
-
14
- var showTS = function (ts) {
15
- return "Timestamp(" + ts.getHighBits() + ", " + ts.getLowBits() + ")";
16
- };
17
-
18
- type OplogRecord = {
19
- ts: Timestamp,
20
- op: "d" | "i" | "u" | "c",
21
- ns: string,
22
- o: { drop: any, dropDatabase: number, applyOps: any }
23
- };
24
-
25
- export function idForOp(op) {
26
- if (op.op === 'd')
27
- return op.o._id;
28
- else if (op.op === 'i')
29
- return op.o._id;
30
- else if (op.op === 'u')
31
- return op.o2._id;
32
- else if (op.op === 'c')
33
- throw Error("Operator 'c' doesn't supply an object with id: " +
34
- stringify(op));
35
- else
36
- throw Error("Unknown op: " + stringify(op));
37
- };
38
-
39
- export class OplogHandle {
40
- private _oplogUrl: string;
41
- private _dbName: string;
42
-
43
- private _oplogLastEntryConnection: LiveMongoConnection;
44
- private _oplogTailConnection: LiveMongoConnection;
45
- private _stopped: boolean;
46
- private _tailHandle: { stop: () => void } | null;
47
- private _crossbar: _Crossbar;
48
- private _baseOplogSelector: Filter<OplogRecord>;
49
- private _catchingUpFutures: { ts: Timestamp, future: any }[];
50
- private _onSkippedEntriesHook: Hook;
51
- private _entryQueue: DoubleEndedQueue<any>;
52
- private _workerActive: boolean;
53
- private _lastProcessedTS: Timestamp;
54
- private _readyFuture: { promise: Promise<void>, resolve: Function };
55
-
56
- constructor (oplogUrl: string, dbName: string) {
57
- var self = this;
58
- self._readyFuture = { promise: undefined, resolve: undefined };
59
- self._readyFuture.promise = new Promise(r => self._readyFuture.resolve = r);
60
- self._oplogUrl = oplogUrl;
61
- self._dbName = dbName;
62
-
63
- self._oplogLastEntryConnection = null;
64
- self._oplogTailConnection = null;
65
- self._stopped = false;
66
- self._tailHandle = null;
67
- self._crossbar = new _Crossbar({
68
- factPackage: "mongo-livedata", factName: "oplog-watchers"
69
- });
70
- self._baseOplogSelector = {
71
- ns: new RegExp("^(?:" + _escapeRegExp(self._dbName) + "\\.|admin\\.$cmd)"),
72
-
73
- $or: [
74
- { op: { $in: ['i', 'u', 'd'] } },
75
- // drop collection
76
- { op: 'c', 'o.drop': { $exists: true } },
77
- { op: 'c', 'o.dropDatabase': 1 },
78
- { op: 'c', 'o.applyOps': { $exists: true } },
79
- ]
80
- };
81
-
82
- // Data structures to support waitUntilCaughtUp(). Each oplog entry has a
83
- // MongoTimestamp object on it (which is not the same as a Date --- it's a
84
- // combination of time and an incrementing counter; see
85
- // http://docs.mongodb.org/manual/reference/bson-types/#timestamps).
86
- //
87
- // _catchingUpFutures is an array of {ts: MongoTimestamp, future: Future}
88
- // objects, sorted by ascending timestamp. _lastProcessedTS is the
89
- // MongoTimestamp of the last oplog entry we've processed.
90
- //
91
- // Each time we call waitUntilCaughtUp, we take a peek at the final oplog
92
- // entry in the db. If we've already processed it (ie, it is not greater than
93
- // _lastProcessedTS), waitUntilCaughtUp immediately returns. Otherwise,
94
- // waitUntilCaughtUp makes a new Future and inserts it along with the final
95
- // timestamp entry that it read, into _catchingUpFutures. waitUntilCaughtUp
96
- // then waits on that future, which is resolved once _lastProcessedTS is
97
- // incremented to be past its timestamp by the worker fiber.
98
- //
99
- // XXX use a priority queue or something else that's faster than an array
100
- self._catchingUpFutures = [];
101
- self._lastProcessedTS = null;
102
-
103
- self._onSkippedEntriesHook = new Hook({
104
- debugPrintExceptions: "onSkippedEntries callback"
105
- });
106
-
107
- self._entryQueue = new DoubleEndedQueue();
108
- self._workerActive = false;
109
-
110
- self._startTailing();
111
- };
112
-
113
- stop() {
114
- var self = this;
115
- if (self._stopped)
116
- return;
117
- self._stopped = true;
118
- if (self._tailHandle)
119
- self._tailHandle.stop();
120
- // XXX should close connections too
121
- }
122
-
123
- async onOplogEntry(trigger: Record<string, any>, callback: (notification: Record<string, any>) => void) {
124
- var self = this;
125
- if (self._stopped)
126
- throw new Error("Called onOplogEntry on stopped handle!");
127
-
128
- // Calling onOplogEntry requires us to wait for the tailing to be ready.
129
- await self._readyFuture.promise;
130
-
131
- var originalCallback = callback;
132
- callback = function (notification) {
133
- try {
134
- originalCallback(notification);
135
- } catch (err) {
136
- console.error("Error in oplog callback", err);
137
- }
138
- }
139
- var listenHandle = self._crossbar.listen(trigger, callback);
140
- return {
141
- stop: function () {
142
- listenHandle.stop();
143
- }
144
- };
145
- }
146
-
147
- // Register a callback to be invoked any time we skip oplog entries (eg,
148
- // because we are too far behind).
149
- onSkippedEntries(callback) {
150
- var self = this;
151
- if (self._stopped)
152
- throw new Error("Called onSkippedEntries on stopped handle!");
153
- return self._onSkippedEntriesHook.register(callback);
154
- }
155
- // Calls `callback` once the oplog has been processed up to a point that is
156
- // roughly "now": specifically, once we've processed all ops that are
157
- // currently visible.
158
- // XXX become convinced that this is actually safe even if oplogConnection
159
- // is some kind of pool
160
- async waitUntilCaughtUp() {
161
- var self = this;
162
- if (self._stopped)
163
- throw new Error("Called waitUntilCaughtUp on stopped handle!");
164
-
165
- // Calling waitUntilCaughtUp requries us to wait for the oplog connection to
166
- // be ready.
167
- await self._readyFuture.promise;
168
- var lastEntry;
169
-
170
- while (!self._stopped) {
171
- // We need to make the selector at least as restrictive as the actual
172
- // tailing selector (ie, we need to specify the DB name) or else we might
173
- // find a TS that won't show up in the actual tail stream.
174
- try {
175
- lastEntry = await self._oplogLastEntryConnection.db.collection<OplogRecord>(OPLOG_COLLECTION).findOne(
176
- self._baseOplogSelector,
177
- { projection: { ts: 1 }, sort: { $natural: -1 } }
178
- );
179
- break;
180
- } catch (e) {
181
- // During failover (eg) if we get an exception we should log and retry
182
- // instead of crashing.
183
- console.warn("Got exception while reading last entry", e);
184
- await _sleepForMs(100);
185
- }
186
- }
187
-
188
- if (self._stopped)
189
- return;
190
-
191
- if (!lastEntry) {
192
- // Really, nothing in the oplog? Well, we've processed everything.
193
- return;
194
- }
195
-
196
- var ts = lastEntry.ts;
197
- if (!ts)
198
- throw Error("oplog entry without ts: " + stringify(lastEntry));
199
-
200
- if (self._lastProcessedTS && ts.lessThanOrEqual(self._lastProcessedTS)) {
201
- // We've already caught up to here.
202
- return;
203
- }
204
-
205
- // Insert the future into our list. Almost always, this will be at the end,
206
- // but it's conceivable that if we fail over from one primary to another,
207
- // the oplog entries we see will go backwards.
208
- var insertAfter = self._catchingUpFutures.length;
209
- while (insertAfter - 1 > 0 && self._catchingUpFutures[insertAfter - 1].ts.greaterThan(ts)) {
210
- insertAfter--;
211
- }
212
- var f = { promise: null, resolve: null };
213
- f.promise = new Promise((r) => f.resolve = r);
214
- self._catchingUpFutures.splice(insertAfter, 0, { ts: ts, future: f });
215
- await f.promise;
216
- }
217
-
218
- async _startTailing() {
219
- var self = this;
220
- // First, make sure that we're talking to the local database.
221
- if (self._oplogUrl.indexOf("/local?") === -1 && !self._oplogUrl.endsWith("/local")) {
222
- throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of " +
223
- "a Mongo replica set");
224
- }
225
-
226
- // We make two separate connections to Mongo. The Node Mongo driver
227
- // implements a naive round-robin connection pool: each "connection" is a
228
- // pool of several (5 by default) TCP connections, and each request is
229
- // rotated through the pools. Tailable cursor queries block on the server
230
- // until there is some data to return (or until a few seconds have
231
- // passed). So if the connection pool used for tailing cursors is the same
232
- // pool used for other queries, the other queries will be delayed by seconds
233
- // 1/5 of the time.
234
- //
235
- // The tail connection will only ever be running a single tail command, so
236
- // it only needs to make one underlying TCP connection.
237
- self._oplogTailConnection = new LiveMongoConnection(self._oplogUrl, { maxPoolSize: 1 });
238
- // XXX better docs, but: it's to get monotonic results
239
- // XXX is it safe to say "if there's an in flight query, just use its
240
- // results"? I don't think so but should consider that
241
- self._oplogLastEntryConnection = new LiveMongoConnection(self._oplogUrl, { maxPoolSize: 1 });
242
-
243
- // Now, make sure that there actually is a repl set here. If not, oplog
244
- // tailing won't ever find anything!
245
- // More on the isMasterDoc
246
- // https://docs.mongodb.com/manual/reference/command/isMaster/
247
- const isMasterDoc = await self._oplogLastEntryConnection.db.admin().command({ ismaster: 1 });
248
-
249
- if (!(isMasterDoc && isMasterDoc.setName)) {
250
- throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of " +
251
- "a Mongo replica set");
252
- }
253
-
254
- // Find the last oplog entry.
255
- var lastOplogEntry = await self._oplogLastEntryConnection.db.collection<OplogRecord>(OPLOG_COLLECTION).findOne(
256
- {},
257
- { sort: { $natural: -1 }, projection: { ts: 1 } }
258
- );
259
-
260
- var oplogSelector = { ...self._baseOplogSelector };
261
- if (lastOplogEntry) {
262
- // Start after the last entry that currently exists.
263
- oplogSelector.ts = { $gt: lastOplogEntry.ts };
264
- // If there are any calls to callWhenProcessedLatest before any other
265
- // oplog entries show up, allow callWhenProcessedLatest to call its
266
- // callback immediately.
267
- self._lastProcessedTS = lastOplogEntry.ts;
268
- }
269
-
270
- var cursorDescription = new CursorDescription(OPLOG_COLLECTION, oplogSelector, { tailable: true });
271
-
272
- // Start tailing the oplog.
273
- //
274
- // We restart the low-level oplog query every 30 seconds if we didn't get a
275
- // doc. This is a workaround for #8598: the Node Mongo driver has at least
276
- // one bug that can lead to query callbacks never getting called (even with
277
- // an error) when leadership failover occur.
278
- self._tailHandle = self._oplogTailConnection.tail(
279
- cursorDescription,
280
- function (doc) {
281
- self._entryQueue.push(doc);
282
- self._maybeStartWorker();
283
- },
284
- TAIL_TIMEOUT
285
- );
286
- self._readyFuture.resolve();
287
- }
288
-
289
- _maybeStartWorker() {
290
- var self = this;
291
- if (self._workerActive) return;
292
- self._workerActive = true;
293
-
294
- setImmediate(function () {
295
- // May be called recursively in case of transactions.
296
- function handleDoc(doc: OplogRecord) {
297
- if (doc.ns === "admin.$cmd") {
298
- if (doc.o.applyOps) {
299
- // This was a successful transaction, so we need to apply the
300
- // operations that were involved.
301
- let nextTimestamp: Omit<Long, "toExtendedJSON" | "_bsontype"> = doc.ts;
302
- doc.o.applyOps.forEach(op => {
303
- // See https://github.com/meteor/meteor/issues/10420.
304
- if (!op.ts) {
305
- op.ts = nextTimestamp;
306
- nextTimestamp = nextTimestamp.add(Long.ONE);
307
- }
308
- handleDoc(op);
309
- });
310
- return;
311
- }
312
- throw new Error("Unknown command " + stringify(doc));
313
- }
314
-
315
- const trigger = {
316
- dropCollection: false,
317
- dropDatabase: false,
318
- op: doc,
319
- collection: undefined,
320
- id: undefined
321
- };
322
-
323
- if (typeof doc.ns === "string" &&
324
- doc.ns.startsWith(self._dbName + ".")) {
325
- trigger.collection = doc.ns.slice(self._dbName.length + 1);
326
- }
327
-
328
- // Is it a special command and the collection name is hidden
329
- // somewhere in operator?
330
- if (trigger.collection === "$cmd") {
331
- if (doc.o.dropDatabase) {
332
- delete trigger.collection;
333
- trigger.dropDatabase = true;
334
- } else if (doc.o.hasOwnProperty("drop")) {
335
- trigger.collection = doc.o.drop;
336
- trigger.dropCollection = true;
337
- trigger.id = null;
338
- } else {
339
- throw Error("Unknown command " + stringify(doc));
340
- }
341
-
342
- } else {
343
- // All other ops have an id.
344
- trigger.id = idForOp(doc);
345
- }
346
-
347
- self._crossbar.fire(trigger);
348
- }
349
-
350
- try {
351
- while (!self._stopped &&
352
- !self._entryQueue.isEmpty()) {
353
- // Are we too far behind? Just tell our observers that they need to
354
- // repoll, and drop our queue.
355
- if (self._entryQueue.length > TOO_FAR_BEHIND) {
356
- var lastEntry = self._entryQueue.pop();
357
- self._entryQueue.clear();
358
-
359
- self._onSkippedEntriesHook.each(function (callback) {
360
- callback();
361
- return true;
362
- });
363
-
364
- // Free any waitUntilCaughtUp() calls that were waiting for us to
365
- // pass something that we just skipped.
366
- self._setLastProcessedTS(lastEntry.ts);
367
- continue;
368
- }
369
-
370
- const doc = self._entryQueue.shift();
371
-
372
- // Fire trigger(s) for this doc.
373
- handleDoc(doc);
374
-
375
- // Now that we've processed this operation, process pending
376
- // sequencers.
377
- if (doc.ts) {
378
- self._setLastProcessedTS(doc.ts);
379
- } else {
380
- throw Error("oplog entry without ts: " + stringify(doc));
381
- }
382
- }
383
- } finally {
384
- self._workerActive = false;
385
- }
386
- });
387
- }
388
-
389
- _setLastProcessedTS(ts) {
390
- var self = this;
391
- self._lastProcessedTS = ts;
392
- while (self._catchingUpFutures.length > 0 && self._catchingUpFutures[0].ts.lessThanOrEqual(self._lastProcessedTS)) {
393
- var sequencer = self._catchingUpFutures.shift();
394
- sequencer.future.return();
395
- }
396
- }
397
-
398
- //Methods used on tests to dinamically change TOO_FAR_BEHIND
399
- _defineTooFarBehind(value) {
400
- TOO_FAR_BEHIND = value;
401
- }
402
- _resetTooFarBehind() {
403
- TOO_FAR_BEHIND = process.env.METEOR_OPLOG_TOO_FAR_BEHIND || 2000;
404
- }
405
-
406
- }
407
-
408
- export async function _sleepForMs(ms: number) {
409
- return new Promise((resolve) => setTimeout(resolve, ms));
410
- }
411
-
412
- export function _escapeRegExp(s: string) {
413
- return s.replace(/[\.*+?^$\\|(){}[\]]/g, '\\$&');
414
- }
@@ -1,124 +0,0 @@
1
- // Converter of the new MongoDB Oplog format (>=5.0) to the one that Meteor
2
- // handles well, i.e., `$set` and `$unset`. The new format is completely new,
3
- // and looks as follows:
4
- //
5
- // { $v: 2, diff: Diff }
6
- //
7
- // where `Diff` is a recursive structure:
8
- //
9
- // {
10
- // // Nested updates (sometimes also represented with an s-field).
11
- // // Example: `{ $set: { 'foo.bar': 1 } }`.
12
- // i: { <key>: <value>, ... },
13
- //
14
- // // Top-level updates.
15
- // // Example: `{ $set: { foo: { bar: 1 } } }`.
16
- // u: { <key>: <value>, ... },
17
- //
18
- // // Unsets.
19
- // // Example: `{ $unset: { foo: '' } }`.
20
- // d: { <key>: false, ... },
21
- //
22
- // // Array operations.
23
- // // Example: `{ $push: { foo: 'bar' } }`.
24
- // s<key>: { a: true, u<index>: <value>, ... },
25
- // ...
26
- //
27
- // // Nested operations (sometimes also represented in the `i` field).
28
- // // Example: `{ $set: { 'foo.bar': 1 } }`.
29
- // s<key>: Diff,
30
- // ...
31
- // }
32
- //
33
- // (all fields are optional).
34
-
35
- function join(prefix, key) {
36
- return prefix ? `${prefix}.${key}` : key;
37
- }
38
-
39
- const arrayOperatorKeyRegex = /^(a|u\d+)$/;
40
-
41
- function isArrayOperatorKey(field) {
42
- return arrayOperatorKeyRegex.test(field);
43
- }
44
-
45
- function isArrayOperator(operator) {
46
- return operator.a === true && Object.keys(operator).every(isArrayOperatorKey);
47
- }
48
-
49
- function flattenObjectInto(target, source, prefix) {
50
- if (Array.isArray(source) || typeof source !== 'object' || source === null) {
51
- target[prefix] = source;
52
- } else {
53
- const entries = Object.entries(source);
54
- if (entries.length) {
55
- entries.forEach(([key, value]) => {
56
- flattenObjectInto(target, value, join(prefix, key));
57
- });
58
- } else {
59
- target[prefix] = source;
60
- }
61
- }
62
- }
63
-
64
- const logDebugMessages = !!process.env.OPLOG_CONVERTER_DEBUG;
65
-
66
- function convertOplogDiff(oplogEntry, diff, prefix) {
67
- if (logDebugMessages) {
68
- console.log(`convertOplogDiff(${JSON.stringify(oplogEntry)}, ${JSON.stringify(diff)}, ${JSON.stringify(prefix)})`);
69
- }
70
-
71
- Object.entries(diff).forEach(([diffKey, value]) => {
72
- if (diffKey === 'd') {
73
- // Handle `$unset`s.
74
- oplogEntry.$unset ??= {};
75
- Object.keys(value).forEach(key => {
76
- oplogEntry.$unset[join(prefix, key)] = true;
77
- });
78
- } else if (diffKey === 'i') {
79
- // Handle (potentially) nested `$set`s.
80
- oplogEntry.$set ??= {};
81
- flattenObjectInto(oplogEntry.$set, value, prefix);
82
- } else if (diffKey === 'u') {
83
- // Handle flat `$set`s.
84
- oplogEntry.$set ??= {};
85
- Object.entries(value).forEach(([key, value]) => {
86
- oplogEntry.$set[join(prefix, key)] = value;
87
- });
88
- } else {
89
- // Handle s-fields.
90
- const key = diffKey.slice(1);
91
- if (isArrayOperator(value)) {
92
- // Array operator.
93
- Object.entries(value).forEach(([position, value]) => {
94
- if (position === 'a') {
95
- return;
96
- }
97
-
98
- const positionKey = join(join(prefix, key), position.slice(1));
99
- if (value === null) {
100
- oplogEntry.$unset ??= {};
101
- oplogEntry.$unset[positionKey] = true;
102
- } else {
103
- oplogEntry.$set ??= {};
104
- oplogEntry.$set[positionKey] = value;
105
- }
106
- });
107
- } else if (key) {
108
- // Nested object.
109
- convertOplogDiff(oplogEntry, value, join(prefix, key));
110
- }
111
- }
112
- });
113
- }
114
-
115
- export function oplogV2V1Converter(oplogEntry) {
116
- // Pass-through v1 and (probably) invalid entries.
117
- if (oplogEntry.$v !== 2 || !oplogEntry.diff) {
118
- return oplogEntry;
119
- }
120
-
121
- const convertedOplogEntry = { $v: 2 };
122
- convertOplogDiff(convertedOplogEntry, oplogEntry.diff, '');
123
- return convertedOplogEntry;
124
- }