mongodb-livedata-server 0.0.1
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.
- package/README.md +63 -0
- package/dist/livedata_server.js +9 -0
- package/dist/meteor/binary-heap/max_heap.js +186 -0
- package/dist/meteor/binary-heap/min_heap.js +17 -0
- package/dist/meteor/binary-heap/min_max_heap.js +48 -0
- package/dist/meteor/callback-hook/hook.js +78 -0
- package/dist/meteor/ddp/crossbar.js +136 -0
- package/dist/meteor/ddp/heartbeat.js +77 -0
- package/dist/meteor/ddp/livedata_server.js +403 -0
- package/dist/meteor/ddp/method-invocation.js +72 -0
- package/dist/meteor/ddp/random-stream.js +100 -0
- package/dist/meteor/ddp/session-collection-view.js +106 -0
- package/dist/meteor/ddp/session-document-view.js +82 -0
- package/dist/meteor/ddp/session.js +570 -0
- package/dist/meteor/ddp/stream_server.js +181 -0
- package/dist/meteor/ddp/subscription.js +347 -0
- package/dist/meteor/ddp/utils.js +104 -0
- package/dist/meteor/ddp/writefence.js +111 -0
- package/dist/meteor/diff-sequence/diff.js +257 -0
- package/dist/meteor/ejson/ejson.js +569 -0
- package/dist/meteor/ejson/stringify.js +119 -0
- package/dist/meteor/ejson/utils.js +42 -0
- package/dist/meteor/id-map/id_map.js +92 -0
- package/dist/meteor/mongo/caching_change_observer.js +94 -0
- package/dist/meteor/mongo/doc_fetcher.js +53 -0
- package/dist/meteor/mongo/geojson_utils.js +41 -0
- package/dist/meteor/mongo/live_connection.js +264 -0
- package/dist/meteor/mongo/live_cursor.js +57 -0
- package/dist/meteor/mongo/minimongo_common.js +2002 -0
- package/dist/meteor/mongo/minimongo_matcher.js +217 -0
- package/dist/meteor/mongo/minimongo_sorter.js +268 -0
- package/dist/meteor/mongo/observe_driver_utils.js +73 -0
- package/dist/meteor/mongo/observe_multiplexer.js +228 -0
- package/dist/meteor/mongo/oplog-observe-driver.js +919 -0
- package/dist/meteor/mongo/oplog_tailing.js +352 -0
- package/dist/meteor/mongo/oplog_v2_converter.js +126 -0
- package/dist/meteor/mongo/polling_observe_driver.js +195 -0
- package/dist/meteor/mongo/synchronous-cursor.js +261 -0
- package/dist/meteor/mongo/synchronous-queue.js +110 -0
- package/dist/meteor/ordered-dict/ordered_dict.js +198 -0
- package/dist/meteor/random/AbstractRandomGenerator.js +92 -0
- package/dist/meteor/random/AleaRandomGenerator.js +90 -0
- package/dist/meteor/random/NodeRandomGenerator.js +42 -0
- package/dist/meteor/random/createAleaGenerator.js +32 -0
- package/dist/meteor/random/createRandom.js +22 -0
- package/dist/meteor/random/main.js +12 -0
- package/livedata_server.ts +3 -0
- package/meteor/LICENSE +28 -0
- package/meteor/binary-heap/max_heap.ts +225 -0
- package/meteor/binary-heap/min_heap.ts +15 -0
- package/meteor/binary-heap/min_max_heap.ts +53 -0
- package/meteor/callback-hook/hook.ts +85 -0
- package/meteor/ddp/crossbar.ts +148 -0
- package/meteor/ddp/heartbeat.ts +97 -0
- package/meteor/ddp/livedata_server.ts +473 -0
- package/meteor/ddp/method-invocation.ts +86 -0
- package/meteor/ddp/random-stream.ts +102 -0
- package/meteor/ddp/session-collection-view.ts +119 -0
- package/meteor/ddp/session-document-view.ts +92 -0
- package/meteor/ddp/session.ts +708 -0
- package/meteor/ddp/stream_server.ts +204 -0
- package/meteor/ddp/subscription.ts +392 -0
- package/meteor/ddp/utils.ts +119 -0
- package/meteor/ddp/writefence.ts +130 -0
- package/meteor/diff-sequence/diff.ts +295 -0
- package/meteor/ejson/ejson.ts +601 -0
- package/meteor/ejson/stringify.ts +122 -0
- package/meteor/ejson/utils.ts +38 -0
- package/meteor/id-map/id_map.ts +84 -0
- package/meteor/mongo/caching_change_observer.ts +120 -0
- package/meteor/mongo/doc_fetcher.ts +52 -0
- package/meteor/mongo/geojson_utils.ts +42 -0
- package/meteor/mongo/live_connection.ts +302 -0
- package/meteor/mongo/live_cursor.ts +79 -0
- package/meteor/mongo/minimongo_common.ts +2440 -0
- package/meteor/mongo/minimongo_matcher.ts +275 -0
- package/meteor/mongo/minimongo_sorter.ts +331 -0
- package/meteor/mongo/observe_driver_utils.ts +79 -0
- package/meteor/mongo/observe_multiplexer.ts +256 -0
- package/meteor/mongo/oplog-observe-driver.ts +1049 -0
- package/meteor/mongo/oplog_tailing.ts +414 -0
- package/meteor/mongo/oplog_v2_converter.ts +124 -0
- package/meteor/mongo/polling_observe_driver.ts +247 -0
- package/meteor/mongo/synchronous-cursor.ts +293 -0
- package/meteor/mongo/synchronous-queue.ts +119 -0
- package/meteor/ordered-dict/ordered_dict.ts +229 -0
- package/meteor/random/AbstractRandomGenerator.ts +99 -0
- package/meteor/random/AleaRandomGenerator.ts +96 -0
- package/meteor/random/NodeRandomGenerator.ts +37 -0
- package/meteor/random/createAleaGenerator.ts +31 -0
- package/meteor/random/createRandom.ts +19 -0
- package/meteor/random/main.ts +8 -0
- package/package.json +30 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,414 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
}
|