@omegup/msync 0.1.21 → 0.1.23
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/index.d.ts +7 -10
- package/index.esm.js +242 -123
- package/index.js +241 -123
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Timestamp, Filter, UpdateFilter, BSON, Db, Collection, IndexSpecification, CreateIndexesOptions, MongoClient as MongoClient$1, OptionalUnlessRequiredId } from 'mongodb';
|
|
1
|
+
import { Timestamp, Filter, UpdateFilter, BSON, MaxKey, Db, Collection, IndexSpecification, CreateIndexesOptions, MongoClient as MongoClient$1, OptionalUnlessRequiredId } from 'mongodb';
|
|
2
2
|
export { Collection, Timestamp } from 'mongodb';
|
|
3
3
|
|
|
4
4
|
type HasJob = {
|
|
@@ -94,7 +94,7 @@ type StreamRunnerParam<in V, out Result> = {
|
|
|
94
94
|
raw: (first: boolean) => RawStages<unknown, V, Result>;
|
|
95
95
|
teardown: <R>(consume: <W, M extends keyof Actions<W>>(x: TeardownRecord<W, M>) => R) => R;
|
|
96
96
|
};
|
|
97
|
-
type StreamRunner<out V> = <Result>(input: StreamRunnerParam<V, Result>) => Runner<readonly Result[], HasJob>;
|
|
97
|
+
type StreamRunner<out V> = <Result>(input: StreamRunnerParam<V, Result>, setup?: () => Promise<void>) => Runner<readonly Result[], HasJob>;
|
|
98
98
|
type SimpleStreamExecutionResult<out Q, out V extends Q> = {
|
|
99
99
|
readonly out: StreamRunner<V>;
|
|
100
100
|
};
|
|
@@ -220,7 +220,7 @@ declare const Type: unique symbol;
|
|
|
220
220
|
|
|
221
221
|
type U = undefined;
|
|
222
222
|
type N = null | U;
|
|
223
|
-
type jsonPrim = number | null | string | boolean | Timestamp | Date;
|
|
223
|
+
type jsonPrim = number | null | string | boolean | Timestamp | MaxKey | Date;
|
|
224
224
|
type notObj = jsonPrim | U;
|
|
225
225
|
type notArr = notObj | O;
|
|
226
226
|
type jsonItem = unknown;
|
|
@@ -536,7 +536,8 @@ declare const log: (...args: unknown[]) => void;
|
|
|
536
536
|
|
|
537
537
|
declare const createIndex: (collection: {
|
|
538
538
|
readonly createIndex: Collection["createIndex"];
|
|
539
|
-
|
|
539
|
+
collectionName: string;
|
|
540
|
+
}, indexSpec: IndexSpecification, op?: CreateIndexesOptions) => Promise<void>;
|
|
540
541
|
|
|
541
542
|
declare const noop: () => void;
|
|
542
543
|
declare const map1: <K extends string, Im>(k: AsLiteral<K>, to: Im) => { readonly [P in K]: [P, Im]; } & {
|
|
@@ -697,7 +698,7 @@ declare const wrap: <Result>(root: Machine<Result>) => Machine<Result>;
|
|
|
697
698
|
declare const $eq: <T extends unknown>(operand: rawItem & T) => Predicate<T>;
|
|
698
699
|
declare const $ne: <T extends unknown>(operand: rawItem & T) => Predicate<T>;
|
|
699
700
|
type Numeric = number | Timestamp | Date;
|
|
700
|
-
declare const comp: <D2 extends Numeric = Numeric>(op: "$lt" | "$
|
|
701
|
+
declare const comp: <D2 extends Numeric = Numeric>(op: "$lt" | "$lte" | "$gt" | "$gte") => <T extends Numeric>(operand: rawItem & D2) => Predicate<D2>;
|
|
701
702
|
declare const $gt: <T extends Numeric>(operand: rawItem & Numeric) => Predicate<Numeric>;
|
|
702
703
|
declare const $gtTs: <T extends Numeric>(operand: rawItem & Timestamp) => Predicate<Timestamp>;
|
|
703
704
|
declare const $gteTs: <T extends Numeric>(operand: rawItem & Timestamp) => Predicate<Timestamp>;
|
|
@@ -730,12 +731,8 @@ declare const $and: Combiner;
|
|
|
730
731
|
declare const $nor: Combiner;
|
|
731
732
|
declare const $or: Combiner;
|
|
732
733
|
|
|
733
|
-
declare const setF: (f: ({ input }: {
|
|
734
|
-
input: any;
|
|
735
|
-
}) => Promise<void>) => void;
|
|
736
|
-
|
|
737
734
|
declare const enablePreAndPostImages: <T extends doc>(coll: Collection<T>) => Promise<Document>;
|
|
738
735
|
declare const prepare: (testName?: string) => Promise<MongoClient$1>;
|
|
739
736
|
declare const makeCol: <T extends ID>(docs: readonly OptionalUnlessRequiredId<T>[], database: Db, name?: string) => Promise<Collection<T>>;
|
|
740
737
|
|
|
741
|
-
export { $accumulator, $and, $countDict, $entries, $eq, $exists, $expr, $getField, $group, $groupId, $groupMerge, $group_, $gt, $gtTs, $gte, $gteTs, $ifNull, $in, $insert, $insertPart, $insertX, $keys, $let, $lookup, $lt, $lte, $map, $map0, $map1, $match, $matchDelta, $merge, $merge2, $mergeId, $mergePart, $merge_, $ne, $nin, $nor, $or, $outerLookup, $pushDict, $rand, $reduce, $replaceWith, $set, $simpleInsert, $simpleMerge, $simpleMergePart, $sum, $type, $unwind, $unwindDelta, type Accumulators, type Arr, type AsLiteral, type Delta, type DeltaAccumulator, type DeltaAccumulators, type ExactKeys, Expr, type ExprHKT, type Exprs, type ExprsExact, type ExprsExactHKT, type ExprsPart, Field, type ID, type Loose, Machine, type Merge, type MergeArgs, type MergeInto, type MergeMapOArgs, type Model, type MongoTypeNames, type N, type NoRaw, type NullToOBJ, type O, type OPick, type OPickD, type Patch, type RONoRaw, type RORec, type RawStages, type Rec, type Replace, type SnapshotStreamExecutionResult, type StrKey, type Strict, type TS, Type, type WriteonlyCollection, add, and, anyElementTrue, array, ceil, comp, concat, concatArray, createIndex, ctx, current, dateAdd, dateDiff, dateLt, datePart, dayAndMonthPart, divide, type doc, enablePreAndPostImages, eq, eqTyped, except, exprMapVal, field, fieldF, fieldM, filter, filterDefined, first, firstSure, floor, from, func, getWhenMatched, getWhenMatchedForMerge, gt, gte, inArray, isArray, ite, type jsonPrim, last, log, lt, lte, makeCol, map1, mapVal, max, maxDate, mergeExact, mergeExact0, mergeExpr, mergeObjects, minDate, monthPart, multiply, ne, nil, noop, not, type notArr, notNull, now, or, pair, prepare, rand, range, regex, root, set,
|
|
738
|
+
export { $accumulator, $and, $countDict, $entries, $eq, $exists, $expr, $getField, $group, $groupId, $groupMerge, $group_, $gt, $gtTs, $gte, $gteTs, $ifNull, $in, $insert, $insertPart, $insertX, $keys, $let, $lookup, $lt, $lte, $map, $map0, $map1, $match, $matchDelta, $merge, $merge2, $mergeId, $mergePart, $merge_, $ne, $nin, $nor, $or, $outerLookup, $pushDict, $rand, $reduce, $replaceWith, $set, $simpleInsert, $simpleMerge, $simpleMergePart, $sum, $type, $unwind, $unwindDelta, type Accumulators, type Arr, type AsLiteral, type Delta, type DeltaAccumulator, type DeltaAccumulators, type ExactKeys, Expr, type ExprHKT, type Exprs, type ExprsExact, type ExprsExactHKT, type ExprsPart, Field, type ID, type Loose, Machine, type Merge, type MergeArgs, type MergeInto, type MergeMapOArgs, type Model, type MongoTypeNames, type N, type NoRaw, type NullToOBJ, type O, type OPick, type OPickD, type Patch, type RONoRaw, type RORec, type RawStages, type Rec, type Replace, type SnapshotStreamExecutionResult, type StrKey, type Strict, type TS, Type, type WriteonlyCollection, add, and, anyElementTrue, array, ceil, comp, concat, concatArray, createIndex, ctx, current, dateAdd, dateDiff, dateLt, datePart, dayAndMonthPart, divide, type doc, enablePreAndPostImages, eq, eqTyped, except, exprMapVal, field, fieldF, fieldM, filter, filterDefined, first, firstSure, floor, from, func, getWhenMatched, getWhenMatchedForMerge, gt, gte, inArray, isArray, ite, type jsonPrim, last, log, lt, lte, makeCol, map1, mapVal, max, maxDate, mergeExact, mergeExact0, mergeExpr, mergeObjects, minDate, monthPart, multiply, ne, nil, noop, not, type notArr, notNull, now, or, pair, prepare, rand, range, regex, root, set, setField, single, size, slice, sortArray, staging, startOf, str, sub, subtract, to, toInt, val, weekPart, wrap, year };
|
package/index.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { MaxKey, UUID, MongoClient } from 'mongodb';
|
|
1
2
|
import crypto$1 from 'crypto';
|
|
2
3
|
import { canonicalize } from 'json-canonicalize';
|
|
3
4
|
import { SynchronousPromise } from 'synchronous-promise';
|
|
4
|
-
import { MongoClient, UUID } from 'mongodb';
|
|
5
5
|
import { writeFile } from 'fs/promises';
|
|
6
6
|
|
|
7
7
|
const asExprRaw = (raw) => ({ get: () => raw });
|
|
@@ -82,7 +82,7 @@ const val = (val) => asExpr({
|
|
|
82
82
|
: val),
|
|
83
83
|
});
|
|
84
84
|
const current = asExpr({
|
|
85
|
-
raw: () => asExprRaw(
|
|
85
|
+
raw: () => asExprRaw(new MaxKey()),
|
|
86
86
|
});
|
|
87
87
|
const $let = (vars, inExpr) => asExpr({
|
|
88
88
|
raw: f => asExprRaw({
|
|
@@ -1251,19 +1251,48 @@ const $lookupRaw = ({ field1, field2 }, { coll, exec, input }, k2, k, includeNul
|
|
|
1251
1251
|
|
|
1252
1252
|
const asBefore = (f) => f(() => root().of('before'));
|
|
1253
1253
|
|
|
1254
|
-
const
|
|
1254
|
+
const T = (s) => `Timestamp(${parseInt(`${BigInt(s) / 2n ** 32n}`)}, ${parseInt(`${BigInt(s) % 2n ** 32n}`)})`;
|
|
1255
|
+
const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
|
|
1256
|
+
const json = (a) => replace(JSON.stringify(a));
|
|
1257
|
+
const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
|
|
1258
|
+
|
|
1259
|
+
const indexMap = new Map();
|
|
1260
|
+
const createIndex = async (collection, indexSpec, op) => {
|
|
1261
|
+
const { name, ...options } = op ?? {};
|
|
1262
|
+
const map = indexMap.get(collection.collectionName) ?? new Map();
|
|
1263
|
+
indexMap.set(collection.collectionName, map);
|
|
1264
|
+
const indexKey = `${JSON.stringify(indexSpec)}-${JSON.stringify(options)}`;
|
|
1265
|
+
if (map.has(indexKey)) {
|
|
1266
|
+
await map.get(indexKey);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const promise = createIndexWithRetry(collection, indexSpec, op);
|
|
1270
|
+
map.set(indexKey, promise);
|
|
1271
|
+
await promise;
|
|
1272
|
+
};
|
|
1273
|
+
const createIndexWithRetry = async (collection, indexSpec, options) => {
|
|
1274
|
+
const log = () => { };
|
|
1275
|
+
log('Creating index', { collection: collection.collectionName, indexSpec, options });
|
|
1255
1276
|
while (true) {
|
|
1256
1277
|
try {
|
|
1257
1278
|
await collection.createIndex(indexSpec, options);
|
|
1279
|
+
log('Index created', { collection: collection.collectionName, indexSpec, options });
|
|
1258
1280
|
}
|
|
1259
1281
|
catch (e) {
|
|
1260
1282
|
if ([85, 276].includes(e.code)) {
|
|
1283
|
+
log('Index created with different name', e.code, { collection: collection.collectionName, indexSpec, options });
|
|
1261
1284
|
break;
|
|
1262
1285
|
}
|
|
1263
1286
|
if (e.code == 12587) {
|
|
1264
1287
|
await new Promise(res => setTimeout(res, 300));
|
|
1265
1288
|
continue;
|
|
1266
1289
|
}
|
|
1290
|
+
log('Error creating index', {
|
|
1291
|
+
collection: collection.collectionName,
|
|
1292
|
+
indexSpec,
|
|
1293
|
+
options,
|
|
1294
|
+
error: e,
|
|
1295
|
+
});
|
|
1267
1296
|
console.error('Error creating index', e);
|
|
1268
1297
|
throw e;
|
|
1269
1298
|
}
|
|
@@ -1304,16 +1333,18 @@ const nextWinner = (previousWinner, previousWinnerNextFrame, sources, interrupt)
|
|
|
1304
1333
|
};
|
|
1305
1334
|
|
|
1306
1335
|
const mergeIterators = (params) => {
|
|
1307
|
-
const { sources, interrupt, select = race } = params;
|
|
1336
|
+
const { sources, interrupt, select = race, hooks } = params;
|
|
1308
1337
|
const reiterate = (winner) => {
|
|
1309
1338
|
const { frame, key } = winner;
|
|
1310
1339
|
return {
|
|
1311
1340
|
cont: () => {
|
|
1312
1341
|
const result = frame.cont();
|
|
1342
|
+
hooks?.start?.(frame, result);
|
|
1313
1343
|
return mergeIterators({
|
|
1314
1344
|
sources: patch(sources, key, result),
|
|
1315
1345
|
interrupt,
|
|
1316
1346
|
select: sources => nextWinner(winner, result.next, sources, interrupt),
|
|
1347
|
+
hooks,
|
|
1317
1348
|
});
|
|
1318
1349
|
},
|
|
1319
1350
|
data: frame.data,
|
|
@@ -1321,7 +1352,7 @@ const mergeIterators = (params) => {
|
|
|
1321
1352
|
};
|
|
1322
1353
|
};
|
|
1323
1354
|
return {
|
|
1324
|
-
stop: () => mergeIterators({ sources: restart(sources), interrupt }),
|
|
1355
|
+
stop: () => mergeIterators({ sources: restart(sources), interrupt, select, hooks }),
|
|
1325
1356
|
next: select(sources).then(reiterate),
|
|
1326
1357
|
clear: async () => {
|
|
1327
1358
|
for (const key in sources) {
|
|
@@ -1331,56 +1362,37 @@ const mergeIterators = (params) => {
|
|
|
1331
1362
|
};
|
|
1332
1363
|
};
|
|
1333
1364
|
|
|
1334
|
-
const
|
|
1335
|
-
const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
|
|
1336
|
-
const json = (a) => replace(JSON.stringify(a));
|
|
1337
|
-
const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
|
|
1338
|
-
|
|
1339
|
-
const state = { steady: false, f: (_) => Promise.resolve() };
|
|
1365
|
+
const state = { steady: false };
|
|
1340
1366
|
let timeout = null;
|
|
1341
|
-
const setF = (f) => {
|
|
1342
|
-
state.f = f;
|
|
1343
|
-
};
|
|
1344
|
-
const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
|
|
1345
|
-
const req = {
|
|
1346
|
-
aggregate: coll.collectionName,
|
|
1347
|
-
pipeline: input,
|
|
1348
|
-
cursor: {},
|
|
1349
|
-
...(snapshot && { readConcern: { level: 'snapshot' } }),
|
|
1350
|
-
};
|
|
1351
|
-
if (timeout !== null) {
|
|
1352
|
-
clearTimeout(timeout);
|
|
1353
|
-
timeout = null;
|
|
1354
|
-
}
|
|
1355
|
-
log('exec', streamName, req);
|
|
1356
|
-
const start2 = Date.now();
|
|
1357
|
-
return db.then(d => d.command(req)).then(result => {
|
|
1358
|
-
log('prepare', streamName, Date.now() - start);
|
|
1359
|
-
log('prepare2', streamName, start2 - start);
|
|
1360
|
-
const r = result;
|
|
1361
|
-
log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
|
|
1362
|
-
if (!state.steady) {
|
|
1363
|
-
if (timeout !== null)
|
|
1364
|
-
throw new Error('timeout should be null');
|
|
1365
|
-
timeout = setTimeout(() => {
|
|
1366
|
-
state.steady = true;
|
|
1367
|
-
console.log('steady');
|
|
1368
|
-
}, 10000);
|
|
1369
|
-
}
|
|
1370
|
-
return r;
|
|
1371
|
-
}, err => {
|
|
1372
|
-
log('err', req, err);
|
|
1373
|
-
throw new Error(err);
|
|
1374
|
-
});
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
1367
|
const firstWorksMerge = (iters) => {
|
|
1378
1368
|
const iterator = () => {
|
|
1379
1369
|
const results = iters.map(iter => iter());
|
|
1380
1370
|
const sources = { ...results };
|
|
1381
1371
|
return mergeIterators({
|
|
1382
1372
|
sources,
|
|
1383
|
-
interrupt: key => state.steady
|
|
1373
|
+
interrupt: key => state.steady,
|
|
1374
|
+
hooks: {
|
|
1375
|
+
start: (frame, result) => {
|
|
1376
|
+
if (!frame.info.job)
|
|
1377
|
+
return;
|
|
1378
|
+
if (timeout !== null) {
|
|
1379
|
+
clearTimeout(timeout);
|
|
1380
|
+
timeout = null;
|
|
1381
|
+
}
|
|
1382
|
+
result.next.then(() => {
|
|
1383
|
+
if (!frame.info.job)
|
|
1384
|
+
return;
|
|
1385
|
+
if (!state.steady) {
|
|
1386
|
+
if (timeout !== null)
|
|
1387
|
+
clearTimeout(timeout);
|
|
1388
|
+
timeout = setTimeout(() => {
|
|
1389
|
+
state.steady = true;
|
|
1390
|
+
console.log('steady');
|
|
1391
|
+
}, 2000);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1384
1396
|
});
|
|
1385
1397
|
};
|
|
1386
1398
|
return iterator;
|
|
@@ -1426,8 +1438,6 @@ const runCont = async (it, cb) => {
|
|
|
1426
1438
|
|
|
1427
1439
|
const merge = ({ lsource: L, rsource: R, }) => mergeIterators({ sources: { L, R } });
|
|
1428
1440
|
const join = ({ lField, rField, left, right, as }, leftSnapshot, rightSnapshot, stagesUntilNextLookup, outerLeft) => {
|
|
1429
|
-
createIndex(leftSnapshot.coll, { [`before.${lField.str()}`]: 1 }).catch(e => e.code == 86 || Promise.reject(e));
|
|
1430
|
-
createIndex(rightSnapshot.coll, { [`before.${rField.str()}`]: 1 }).catch(e => e.code == 86 || Promise.reject(e));
|
|
1431
1441
|
const rightJoinField = { field1: lField, field2: rField };
|
|
1432
1442
|
const joinId = 'left';
|
|
1433
1443
|
const joinR_Snapshot = asBefore($lookupRaw(rightJoinField, rightSnapshot, as, joinId, outerLeft));
|
|
@@ -1447,6 +1457,15 @@ const join = ({ lField, rField, left, right, as }, leftSnapshot, rightSnapshot,
|
|
|
1447
1457
|
const getRunner = (f, stages, final) => f.out({
|
|
1448
1458
|
raw: first => concatStages(stages, final.raw(first)),
|
|
1449
1459
|
teardown: final.teardown,
|
|
1460
|
+
}, async () => {
|
|
1461
|
+
log('Creating indexes for lookup left', leftSnapshot.coll.collectionName, {
|
|
1462
|
+
[`before.${lField.str()}`]: 1,
|
|
1463
|
+
});
|
|
1464
|
+
await createIndex(leftSnapshot.coll, { [`before.${lField.str()}`]: 1 }, { name: 'left_' + new UUID().toString('base64') });
|
|
1465
|
+
log('Creating indexes for lookup right', rightSnapshot.coll.collectionName, {
|
|
1466
|
+
[`before.${rField.str()}`]: 1,
|
|
1467
|
+
});
|
|
1468
|
+
await createIndex(rightSnapshot.coll, { [`before.${rField.str()}`]: 1 }, { name: 'right_' + new UUID().toString('base64') });
|
|
1450
1469
|
});
|
|
1451
1470
|
const lRunner = getRunner(left, lRunnerInput, finalInput);
|
|
1452
1471
|
const rRunner = getRunner(right, rRunnerInput, finalInput);
|
|
@@ -1510,10 +1529,7 @@ const $insertX = (out, expr, map, ext, extExpr) => {
|
|
|
1510
1529
|
teardown: c => c({
|
|
1511
1530
|
collection: out,
|
|
1512
1531
|
method: 'updateMany',
|
|
1513
|
-
params: [
|
|
1514
|
-
filter,
|
|
1515
|
-
[{ $set: { deletedAt: '$$NOW', touchedAt: '$$CLUSTER_TIME' } }],
|
|
1516
|
-
],
|
|
1532
|
+
params: [filter, [{ $set: { deletedAt: '$$NOW', touchedAt: '$$CLUSTER_TIME' } }]],
|
|
1517
1533
|
}),
|
|
1518
1534
|
raw: () => {
|
|
1519
1535
|
const replacer = map(mergeObjects(expr, field(mergeExpr(extExpr, {
|
|
@@ -1544,6 +1560,27 @@ const $insertPart = (out, ext) => {
|
|
|
1544
1560
|
const $insert = (out) => $insertPart(out, {});
|
|
1545
1561
|
const assertNotNull = (expr) => expr;
|
|
1546
1562
|
|
|
1563
|
+
const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
|
|
1564
|
+
const req = {
|
|
1565
|
+
aggregate: coll.collectionName,
|
|
1566
|
+
pipeline: input,
|
|
1567
|
+
cursor: {},
|
|
1568
|
+
...(snapshot && { readConcern: { level: 'snapshot' } }),
|
|
1569
|
+
};
|
|
1570
|
+
log('exec', streamName, req);
|
|
1571
|
+
const start2 = Date.now();
|
|
1572
|
+
return db.then(d => d.command(req)).then(result => {
|
|
1573
|
+
log('prepare', streamName, Date.now() - start);
|
|
1574
|
+
log('prepare2', streamName, start2 - start);
|
|
1575
|
+
const r = result;
|
|
1576
|
+
log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
|
|
1577
|
+
return r;
|
|
1578
|
+
}, err => {
|
|
1579
|
+
log('err', req, err);
|
|
1580
|
+
throw new Error(err);
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1547
1584
|
const addTeardown = (it, tr) => {
|
|
1548
1585
|
if (!tr)
|
|
1549
1586
|
return it;
|
|
@@ -1571,7 +1608,7 @@ async function getLastCommittedTs(adminDb) {
|
|
|
1571
1608
|
const st = await adminDb.command({ replSetGetStatus: 1 });
|
|
1572
1609
|
return st?.optimes?.lastCommittedOpTime?.ts ?? null;
|
|
1573
1610
|
}
|
|
1574
|
-
async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
|
|
1611
|
+
async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000 } = {}) {
|
|
1575
1612
|
const adminDb = db.client.db('admin');
|
|
1576
1613
|
const deadline = Date.now() + timeoutMs;
|
|
1577
1614
|
while (true) {
|
|
@@ -1579,7 +1616,7 @@ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
|
|
|
1579
1616
|
if (stable && stable.comp(oplogTs) >= 0)
|
|
1580
1617
|
return;
|
|
1581
1618
|
if (Date.now() > deadline) {
|
|
1582
|
-
throw new Error(
|
|
1619
|
+
throw new Error('Timed out waiting for stable timestamp to reach oplog event time');
|
|
1583
1620
|
}
|
|
1584
1621
|
await sleep(pollMs);
|
|
1585
1622
|
}
|
|
@@ -1601,29 +1638,37 @@ async function* tailOplog(db, opts) {
|
|
|
1601
1638
|
try {
|
|
1602
1639
|
for await (const doc of cursor) {
|
|
1603
1640
|
lastTs = doc.ts;
|
|
1604
|
-
if (doc.op === 'i') {
|
|
1605
|
-
|
|
1641
|
+
if (doc.op === 'i' || '_id' in doc.o) {
|
|
1642
|
+
const fields = new Set(Object.keys(doc.o));
|
|
1643
|
+
fields.delete('_id');
|
|
1644
|
+
yield { fields, doc, changeTouched: doc.o['touchedAt'] instanceof MaxKey };
|
|
1606
1645
|
}
|
|
1607
1646
|
else {
|
|
1647
|
+
let changeTouched = false;
|
|
1608
1648
|
if (doc.o['$v'] !== 2) {
|
|
1609
|
-
throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc
|
|
1649
|
+
throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc)}`);
|
|
1610
1650
|
}
|
|
1611
1651
|
const updatedFields = [];
|
|
1612
1652
|
const diff = doc.o['diff'];
|
|
1613
1653
|
for (const updateOp in diff) {
|
|
1614
1654
|
if (['u', 'i', 'd'].includes(updateOp)) {
|
|
1615
1655
|
updatedFields.push(...Object.keys(diff[updateOp]));
|
|
1656
|
+
if (diff[updateOp]['touchedAt'] instanceof MaxKey) {
|
|
1657
|
+
changeTouched = true;
|
|
1658
|
+
}
|
|
1616
1659
|
}
|
|
1617
1660
|
else if (updateOp.startsWith('s')) {
|
|
1618
1661
|
updatedFields.push(updateOp.slice(1));
|
|
1619
1662
|
}
|
|
1620
1663
|
}
|
|
1621
|
-
yield {
|
|
1664
|
+
yield { fields: new Set(updatedFields), doc, changeTouched };
|
|
1622
1665
|
}
|
|
1623
1666
|
}
|
|
1624
1667
|
}
|
|
1625
1668
|
catch (e) {
|
|
1626
|
-
log('oplog loop error
|
|
1669
|
+
log('oplog loop error, notifying watchers and reopening');
|
|
1670
|
+
console.error(e);
|
|
1671
|
+
yield null;
|
|
1627
1672
|
}
|
|
1628
1673
|
finally {
|
|
1629
1674
|
log('oplog loop ended');
|
|
@@ -1634,15 +1679,68 @@ async function* tailOplog(db, opts) {
|
|
|
1634
1679
|
}
|
|
1635
1680
|
const watchers = new Map();
|
|
1636
1681
|
let running = false;
|
|
1682
|
+
const makePromise = () => {
|
|
1683
|
+
let resolve = () => { };
|
|
1684
|
+
let promise = new Promise(r => (resolve = r));
|
|
1685
|
+
return { promise, resolve };
|
|
1686
|
+
};
|
|
1637
1687
|
const loop = async (db) => {
|
|
1638
1688
|
log('starting oplog loop');
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1689
|
+
let notify = makePromise();
|
|
1690
|
+
let batch = [];
|
|
1691
|
+
const run = async () => {
|
|
1692
|
+
for await (const event of tailOplog(db, {})) {
|
|
1693
|
+
if (event?.fields.size === 0)
|
|
1694
|
+
continue;
|
|
1695
|
+
batch = event && batch ? [...batch, event] : null;
|
|
1696
|
+
notify.resolve();
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
run();
|
|
1700
|
+
const iter = async function* () {
|
|
1701
|
+
while (true) {
|
|
1702
|
+
await notify.promise;
|
|
1703
|
+
const b = batch;
|
|
1704
|
+
batch = [];
|
|
1705
|
+
notify = makePromise();
|
|
1706
|
+
yield b;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
for await (const events of iter()) {
|
|
1710
|
+
if (!events) {
|
|
1711
|
+
log('notifying watchers of oplog loop restart');
|
|
1712
|
+
for (const m of watchers.values()) {
|
|
1713
|
+
for (const { cb } of m.values()) {
|
|
1714
|
+
cb(null);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1642
1717
|
continue;
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1718
|
+
}
|
|
1719
|
+
const groups = Object.groupBy(events.filter(e => e.changeTouched), ev => ev.doc.ns);
|
|
1720
|
+
for (const [ns, evs] of Object.entries(groups)) {
|
|
1721
|
+
if (!evs)
|
|
1722
|
+
continue;
|
|
1723
|
+
const [dbName, collName] = ns.split('.');
|
|
1724
|
+
if (dbName !== db.databaseName)
|
|
1725
|
+
continue;
|
|
1726
|
+
const coll = db.collection(collName);
|
|
1727
|
+
coll
|
|
1728
|
+
.bulkWrite(evs.map((e) => ({
|
|
1729
|
+
updateOne: {
|
|
1730
|
+
filter: { _id: e.doc.o['_id'] ?? e.doc.o2?._id },
|
|
1731
|
+
update: { $set: { touchedAt: e.doc.ts } },
|
|
1732
|
+
},
|
|
1733
|
+
})))
|
|
1734
|
+
.catch(() => { });
|
|
1735
|
+
}
|
|
1736
|
+
for (const { fields, doc } of events) {
|
|
1737
|
+
const m = watchers.get(doc.ns);
|
|
1738
|
+
if (!m)
|
|
1739
|
+
continue;
|
|
1740
|
+
for (const { cb, keys } of m.values()) {
|
|
1741
|
+
if (!keys || keys.some(k => fields.has(k))) {
|
|
1742
|
+
cb(doc);
|
|
1743
|
+
}
|
|
1646
1744
|
}
|
|
1647
1745
|
}
|
|
1648
1746
|
}
|
|
@@ -1664,11 +1762,16 @@ const register = (coll, keys, cb) => {
|
|
|
1664
1762
|
watchers.delete(ns);
|
|
1665
1763
|
};
|
|
1666
1764
|
};
|
|
1667
|
-
|
|
1765
|
+
let maxKeysRemoved = null;
|
|
1766
|
+
const makeWatchStream = async ({ collection, projection: p, hardMatch: m }, streamName) => {
|
|
1767
|
+
const { db } = collection.s;
|
|
1768
|
+
await (maxKeysRemoved ??= Promise.all((await db.listCollections({}, { nameOnly: true }).toArray()).map(x => void db
|
|
1769
|
+
.collection(x.name)
|
|
1770
|
+
.updateMany({ touchedAt: new MaxKey() }, [{ $set: { touchedAt: '$$CLUSTER_TIME' } }]))).then(() => { }));
|
|
1668
1771
|
const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
|
|
1669
1772
|
let resolve = (_) => { };
|
|
1670
1773
|
const promise = new Promise(r => (resolve = r));
|
|
1671
|
-
const close = register(collection, p ? Object.keys(projection) : null,
|
|
1774
|
+
const close = register(collection, p ? Object.keys(projection) : null, doc => {
|
|
1672
1775
|
log(streamName, 'change detected', doc);
|
|
1673
1776
|
resolve(doc);
|
|
1674
1777
|
close();
|
|
@@ -1677,9 +1780,10 @@ const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName
|
|
|
1677
1780
|
tryNext: async () => {
|
|
1678
1781
|
const doc = await promise;
|
|
1679
1782
|
const start = Date.now();
|
|
1680
|
-
|
|
1783
|
+
if (doc)
|
|
1784
|
+
await waitUntilStablePast(collection.s.db, doc.ts);
|
|
1681
1785
|
log(streamName, 'stable past took', Date.now() - start);
|
|
1682
|
-
return doc;
|
|
1786
|
+
return doc ?? {};
|
|
1683
1787
|
},
|
|
1684
1788
|
close: async () => close(),
|
|
1685
1789
|
};
|
|
@@ -1773,44 +1877,17 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1773
1877
|
streamNames[streamName] = hash;
|
|
1774
1878
|
else if (streamNames[streamName] != hash)
|
|
1775
1879
|
throw new Error(`streamName ${streamName} already used`);
|
|
1776
|
-
db.command({
|
|
1777
|
-
collMod: coll,
|
|
1778
|
-
changeStreamPreAndPostImages: { enabled: true },
|
|
1779
|
-
});
|
|
1780
|
-
createIndex(collection, { touchedAt: 1 }, hardMatch
|
|
1781
|
-
? {
|
|
1782
|
-
partialFilterExpression: hardMatch.raw(root()),
|
|
1783
|
-
name: 'touchedAt_hard_' + new UUID().toString('base64'),
|
|
1784
|
-
}
|
|
1785
|
-
: {}).catch(e => e.code == 86 || Promise.reject(e));
|
|
1786
1880
|
const last = db.collection('__last');
|
|
1787
1881
|
const snapshotCollection = db.collection(coll + '_' + streamName + '_snapshot');
|
|
1788
|
-
createIndex(snapshotCollection, { before: 1 }, {
|
|
1789
|
-
partialFilterExpression: { before: null },
|
|
1790
|
-
name: 'before_' + new UUID().toString('base64'),
|
|
1791
|
-
});
|
|
1792
|
-
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1793
|
-
partialFilterExpression: { updated: true },
|
|
1794
|
-
name: 'updated_' + new UUID().toString('base64'),
|
|
1795
|
-
});
|
|
1796
|
-
createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1797
|
-
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1798
|
-
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
1799
|
-
});
|
|
1800
|
-
createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1801
|
-
partialFilterExpression: { updated: true, after: null },
|
|
1802
|
-
name: 'updated_no_after_' + new UUID().toString('base64'),
|
|
1803
|
-
});
|
|
1804
|
-
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1805
|
-
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1806
|
-
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
1807
|
-
});
|
|
1808
1882
|
const job = {};
|
|
1809
|
-
const run = (finalInput) => {
|
|
1810
|
-
const
|
|
1811
|
-
snapshotCollection.drop().catch(noop)
|
|
1812
|
-
|
|
1813
|
-
|
|
1883
|
+
const run = (finalInput, setup) => {
|
|
1884
|
+
const dropSnapshot = async () => {
|
|
1885
|
+
await snapshotCollection.drop().catch(noop);
|
|
1886
|
+
log('snapshot collection dropped', streamName, `db['${snapshotCollection.collectionName}'].drop()`);
|
|
1887
|
+
log('with', [...(indexMap.get(snapshotCollection.collectionName)?.keys() ?? [])], 'indexes in map before deletion');
|
|
1888
|
+
indexMap.delete(snapshotCollection.collectionName);
|
|
1889
|
+
};
|
|
1890
|
+
const clear = async () => Promise.all([dropSnapshot(), last.deleteOne({ _id: streamName })]);
|
|
1814
1891
|
const withStop = (next, tr) => {
|
|
1815
1892
|
return addTeardown(() => ({ stop, next: next(), clear }), tr);
|
|
1816
1893
|
};
|
|
@@ -1835,7 +1912,9 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1835
1912
|
const step0 = () => SynchronousPromise.resolve(next(step1, 'empty new collection'));
|
|
1836
1913
|
const stop = withStop(step0);
|
|
1837
1914
|
const step1 = async () => {
|
|
1915
|
+
log('reset collection', streamName, `db['${snapshotCollection.collectionName}'].updateMany( updated: true }, { $set: { updated: false, after: null } })`);
|
|
1838
1916
|
await snapshotCollection.updateMany({ updated: true }, { $set: { updated: false, after: null } });
|
|
1917
|
+
log('reset collection done', streamName);
|
|
1839
1918
|
return next(step2, 'get last update');
|
|
1840
1919
|
};
|
|
1841
1920
|
const step2 = () => Promise.all([
|
|
@@ -1858,13 +1937,45 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1858
1937
|
};
|
|
1859
1938
|
const [action, out] = actions[method](collection, params);
|
|
1860
1939
|
log('teardown', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
|
|
1861
|
-
await Promise.all([
|
|
1940
|
+
await Promise.all([dropSnapshot(), action]);
|
|
1862
1941
|
log('teardown done', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
|
|
1863
1942
|
};
|
|
1864
1943
|
if (!same) {
|
|
1865
1944
|
log('not same, new data', streamName, data);
|
|
1866
1945
|
await handleTeardown(exists ?? { data });
|
|
1867
1946
|
}
|
|
1947
|
+
log('creating indexes');
|
|
1948
|
+
await createIndex(snapshotCollection, { before: 1 }, {
|
|
1949
|
+
partialFilterExpression: { before: null },
|
|
1950
|
+
name: 'before_' + new UUID().toString('base64'),
|
|
1951
|
+
});
|
|
1952
|
+
await createIndex(snapshotCollection, { updated: 1 }, {
|
|
1953
|
+
partialFilterExpression: { updated: true },
|
|
1954
|
+
name: 'updated_' + new UUID().toString('base64'),
|
|
1955
|
+
});
|
|
1956
|
+
await createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1957
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1958
|
+
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
1959
|
+
});
|
|
1960
|
+
await createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1961
|
+
partialFilterExpression: { updated: true, after: null },
|
|
1962
|
+
name: 'updated_no_after_' + new UUID().toString('base64'),
|
|
1963
|
+
});
|
|
1964
|
+
await createIndex(snapshotCollection, { updated: 1 }, {
|
|
1965
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1966
|
+
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
1967
|
+
});
|
|
1968
|
+
await db.command({
|
|
1969
|
+
collMod: coll,
|
|
1970
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1971
|
+
});
|
|
1972
|
+
await createIndex(collection, { touchedAt: 1 }, hardMatch
|
|
1973
|
+
? {
|
|
1974
|
+
partialFilterExpression: hardMatch.raw(root()),
|
|
1975
|
+
name: 'touchedAt_hard_' + new UUID().toString('base64'),
|
|
1976
|
+
}
|
|
1977
|
+
: {});
|
|
1978
|
+
await setup?.();
|
|
1868
1979
|
await after?.();
|
|
1869
1980
|
return nextData([])(async () => {
|
|
1870
1981
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -1904,7 +2015,8 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1904
2015
|
const first = ts === undefined;
|
|
1905
2016
|
const stages = finalInput.raw(first);
|
|
1906
2017
|
await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
|
|
1907
|
-
const stream = makeStream();
|
|
2018
|
+
const stream = await makeStream();
|
|
2019
|
+
const nextRes = stream.tryNext();
|
|
1908
2020
|
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1909
2021
|
coll: snapshotCollection,
|
|
1910
2022
|
input: link()
|
|
@@ -1918,7 +2030,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1918
2030
|
.with(input.delta)
|
|
1919
2031
|
.with(stages).stages,
|
|
1920
2032
|
}), false, start);
|
|
1921
|
-
const nextRes = stream.tryNext();
|
|
1922
2033
|
stages.at(-1).$merge.into.coll;
|
|
1923
2034
|
return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
|
|
1924
2035
|
};
|
|
@@ -1997,14 +2108,6 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1997
2108
|
: pre;
|
|
1998
2109
|
const job = {};
|
|
1999
2110
|
const db = collection.s.db, coll = collection.collectionName;
|
|
2000
|
-
db.command({
|
|
2001
|
-
collMod: coll,
|
|
2002
|
-
changeStreamPreAndPostImages: { enabled: true },
|
|
2003
|
-
});
|
|
2004
|
-
createIndex(collection, { touchedAt: 1 }, {
|
|
2005
|
-
partialFilterExpression: { deletedAt: { $eq: null } },
|
|
2006
|
-
name: 'touchedAt_' + new UUID().toString('base64'),
|
|
2007
|
-
});
|
|
2008
2111
|
const last = db.collection('__last');
|
|
2009
2112
|
const projectInput = projection &&
|
|
2010
2113
|
$project_(spread(projection, {
|
|
@@ -2041,10 +2144,26 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
2041
2144
|
};
|
|
2042
2145
|
const step0 = () => SynchronousPromise.resolve(next(step1, 'get last update'));
|
|
2043
2146
|
const stop = withStop(step0);
|
|
2044
|
-
const step1 = () =>
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2147
|
+
const step1 = async () => {
|
|
2148
|
+
log('creating indexes');
|
|
2149
|
+
await db.command({
|
|
2150
|
+
collMod: coll,
|
|
2151
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
2152
|
+
});
|
|
2153
|
+
await createIndex(collection, { touchedAt: 1 }, {
|
|
2154
|
+
partialFilterExpression: { deletedAt: { $eq: null } },
|
|
2155
|
+
name: 'touchedAt_' + new UUID().toString('base64'),
|
|
2156
|
+
});
|
|
2157
|
+
log('start stream', { streamName, data });
|
|
2158
|
+
await last.findOne();
|
|
2159
|
+
console.log('got last update');
|
|
2160
|
+
const p = last.findOne({ _id: streamName, data });
|
|
2161
|
+
await p;
|
|
2162
|
+
log('stream started', { streamName, data });
|
|
2163
|
+
const ts = await Promise.all([p, last.findOne({ _id: streamName })]);
|
|
2164
|
+
log('got last update', { streamName, ts });
|
|
2165
|
+
return next(step2_5(ts), 'handle teardown');
|
|
2166
|
+
};
|
|
2048
2167
|
const step2_5 = ([same, exists]) => async () => {
|
|
2049
2168
|
const handleTeardown = async (last) => {
|
|
2050
2169
|
if (!last.data)
|
|
@@ -2074,7 +2193,7 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
2074
2193
|
const makeStream = () => makeWatchStream(view, streamName);
|
|
2075
2194
|
const step4 = (lastTS) => async () => {
|
|
2076
2195
|
const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
|
|
2077
|
-
const stream = makeStream();
|
|
2196
|
+
const stream = await makeStream();
|
|
2078
2197
|
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
2079
2198
|
coll: collection,
|
|
2080
2199
|
input: raw,
|
|
@@ -2119,4 +2238,4 @@ const executes = (view, input, needs) => {
|
|
|
2119
2238
|
};
|
|
2120
2239
|
const single = (view, needs = {}) => pipe(input => executes(view, input, needs), emptyDelta(), concatDelta, emptyDelta);
|
|
2121
2240
|
|
|
2122
|
-
export { $accumulator, $and, $countDict, $entries, $eq, $exists, $expr, $getField, $group, $groupId, $groupMerge, $group_, $gt, $gtTs, $gte, $gteTs, $ifNull, $in, $insert, $insertPart, $insertX, $keys, $let, $lookup, $lt, $lte, $map, $map0, $map1, $match, $matchDelta, $merge, $merge2, $mergeId, $mergePart, $merge_, $ne, $nin, $nor, $or, $outerLookup, $pushDict, $rand, $reduce, $replaceWith, $set, $simpleInsert, $simpleMerge, $simpleMergePart, $sum, $type, $unwind, $unwindDelta, Field, Machine, add, and, anyElementTrue, array, ceil, comp, concat$1 as concat, concatArray, createIndex, ctx, current, dateAdd, dateDiff, dateLt, datePart, dayAndMonthPart, divide, enablePreAndPostImages, eq, eqTyped, except, exprMapVal, field, fieldF, fieldM, filter, filterDefined, first$1 as first, firstSure, floor, from, func, getWhenMatched, getWhenMatchedForMerge, gt, gte, inArray, isArray, ite, last, log, lt, lte, makeCol, map1, mapVal, max, maxDate, mergeExact, mergeExact0, mergeExpr, mergeObjects, minDate, monthPart, multiply, ne, nil, noop, not, notNull, now, or, pair, prepare, rand, range, regex, root, set,
|
|
2241
|
+
export { $accumulator, $and, $countDict, $entries, $eq, $exists, $expr, $getField, $group, $groupId, $groupMerge, $group_, $gt, $gtTs, $gte, $gteTs, $ifNull, $in, $insert, $insertPart, $insertX, $keys, $let, $lookup, $lt, $lte, $map, $map0, $map1, $match, $matchDelta, $merge, $merge2, $mergeId, $mergePart, $merge_, $ne, $nin, $nor, $or, $outerLookup, $pushDict, $rand, $reduce, $replaceWith, $set, $simpleInsert, $simpleMerge, $simpleMergePart, $sum, $type, $unwind, $unwindDelta, Field, Machine, add, and, anyElementTrue, array, ceil, comp, concat$1 as concat, concatArray, createIndex, ctx, current, dateAdd, dateDiff, dateLt, datePart, dayAndMonthPart, divide, enablePreAndPostImages, eq, eqTyped, except, exprMapVal, field, fieldF, fieldM, filter, filterDefined, first$1 as first, firstSure, floor, from, func, getWhenMatched, getWhenMatchedForMerge, gt, gte, inArray, isArray, ite, last, log, lt, lte, makeCol, map1, mapVal, max, maxDate, mergeExact, mergeExact0, mergeExpr, mergeObjects, minDate, monthPart, multiply, ne, nil, noop, not, notNull, now, or, pair, prepare, rand, range, regex, root, set, setField, single, size, slice, sortArray, staging, startOf, str, sub, subtract, to, toInt, val, weekPart, wrap, year };
|
package/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var mongodb = require('mongodb');
|
|
3
4
|
var crypto$1 = require('crypto');
|
|
4
5
|
var jsonCanonicalize = require('json-canonicalize');
|
|
5
6
|
var synchronousPromise = require('synchronous-promise');
|
|
6
|
-
var mongodb = require('mongodb');
|
|
7
7
|
var promises = require('fs/promises');
|
|
8
8
|
|
|
9
9
|
const asExprRaw = (raw) => ({ get: () => raw });
|
|
@@ -84,7 +84,7 @@ const val = (val) => asExpr({
|
|
|
84
84
|
: val),
|
|
85
85
|
});
|
|
86
86
|
const current = asExpr({
|
|
87
|
-
raw: () => asExprRaw(
|
|
87
|
+
raw: () => asExprRaw(new mongodb.MaxKey()),
|
|
88
88
|
});
|
|
89
89
|
const $let = (vars, inExpr) => asExpr({
|
|
90
90
|
raw: f => asExprRaw({
|
|
@@ -1253,19 +1253,48 @@ const $lookupRaw = ({ field1, field2 }, { coll, exec, input }, k2, k, includeNul
|
|
|
1253
1253
|
|
|
1254
1254
|
const asBefore = (f) => f(() => root().of('before'));
|
|
1255
1255
|
|
|
1256
|
-
const
|
|
1256
|
+
const T = (s) => `Timestamp(${parseInt(`${BigInt(s) / 2n ** 32n}`)}, ${parseInt(`${BigInt(s) % 2n ** 32n}`)})`;
|
|
1257
|
+
const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
|
|
1258
|
+
const json = (a) => replace(JSON.stringify(a));
|
|
1259
|
+
const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
|
|
1260
|
+
|
|
1261
|
+
const indexMap = new Map();
|
|
1262
|
+
const createIndex = async (collection, indexSpec, op) => {
|
|
1263
|
+
const { name, ...options } = op ?? {};
|
|
1264
|
+
const map = indexMap.get(collection.collectionName) ?? new Map();
|
|
1265
|
+
indexMap.set(collection.collectionName, map);
|
|
1266
|
+
const indexKey = `${JSON.stringify(indexSpec)}-${JSON.stringify(options)}`;
|
|
1267
|
+
if (map.has(indexKey)) {
|
|
1268
|
+
await map.get(indexKey);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const promise = createIndexWithRetry(collection, indexSpec, op);
|
|
1272
|
+
map.set(indexKey, promise);
|
|
1273
|
+
await promise;
|
|
1274
|
+
};
|
|
1275
|
+
const createIndexWithRetry = async (collection, indexSpec, options) => {
|
|
1276
|
+
const log = () => { };
|
|
1277
|
+
log('Creating index', { collection: collection.collectionName, indexSpec, options });
|
|
1257
1278
|
while (true) {
|
|
1258
1279
|
try {
|
|
1259
1280
|
await collection.createIndex(indexSpec, options);
|
|
1281
|
+
log('Index created', { collection: collection.collectionName, indexSpec, options });
|
|
1260
1282
|
}
|
|
1261
1283
|
catch (e) {
|
|
1262
1284
|
if ([85, 276].includes(e.code)) {
|
|
1285
|
+
log('Index created with different name', e.code, { collection: collection.collectionName, indexSpec, options });
|
|
1263
1286
|
break;
|
|
1264
1287
|
}
|
|
1265
1288
|
if (e.code == 12587) {
|
|
1266
1289
|
await new Promise(res => setTimeout(res, 300));
|
|
1267
1290
|
continue;
|
|
1268
1291
|
}
|
|
1292
|
+
log('Error creating index', {
|
|
1293
|
+
collection: collection.collectionName,
|
|
1294
|
+
indexSpec,
|
|
1295
|
+
options,
|
|
1296
|
+
error: e,
|
|
1297
|
+
});
|
|
1269
1298
|
console.error('Error creating index', e);
|
|
1270
1299
|
throw e;
|
|
1271
1300
|
}
|
|
@@ -1306,16 +1335,18 @@ const nextWinner = (previousWinner, previousWinnerNextFrame, sources, interrupt)
|
|
|
1306
1335
|
};
|
|
1307
1336
|
|
|
1308
1337
|
const mergeIterators = (params) => {
|
|
1309
|
-
const { sources, interrupt, select = race } = params;
|
|
1338
|
+
const { sources, interrupt, select = race, hooks } = params;
|
|
1310
1339
|
const reiterate = (winner) => {
|
|
1311
1340
|
const { frame, key } = winner;
|
|
1312
1341
|
return {
|
|
1313
1342
|
cont: () => {
|
|
1314
1343
|
const result = frame.cont();
|
|
1344
|
+
hooks?.start?.(frame, result);
|
|
1315
1345
|
return mergeIterators({
|
|
1316
1346
|
sources: patch(sources, key, result),
|
|
1317
1347
|
interrupt,
|
|
1318
1348
|
select: sources => nextWinner(winner, result.next, sources, interrupt),
|
|
1349
|
+
hooks,
|
|
1319
1350
|
});
|
|
1320
1351
|
},
|
|
1321
1352
|
data: frame.data,
|
|
@@ -1323,7 +1354,7 @@ const mergeIterators = (params) => {
|
|
|
1323
1354
|
};
|
|
1324
1355
|
};
|
|
1325
1356
|
return {
|
|
1326
|
-
stop: () => mergeIterators({ sources: restart(sources), interrupt }),
|
|
1357
|
+
stop: () => mergeIterators({ sources: restart(sources), interrupt, select, hooks }),
|
|
1327
1358
|
next: select(sources).then(reiterate),
|
|
1328
1359
|
clear: async () => {
|
|
1329
1360
|
for (const key in sources) {
|
|
@@ -1333,56 +1364,37 @@ const mergeIterators = (params) => {
|
|
|
1333
1364
|
};
|
|
1334
1365
|
};
|
|
1335
1366
|
|
|
1336
|
-
const
|
|
1337
|
-
const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
|
|
1338
|
-
const json = (a) => replace(JSON.stringify(a));
|
|
1339
|
-
const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
|
|
1340
|
-
|
|
1341
|
-
const state = { steady: false, f: (_) => Promise.resolve() };
|
|
1367
|
+
const state = { steady: false };
|
|
1342
1368
|
let timeout = null;
|
|
1343
|
-
const setF = (f) => {
|
|
1344
|
-
state.f = f;
|
|
1345
|
-
};
|
|
1346
|
-
const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
|
|
1347
|
-
const req = {
|
|
1348
|
-
aggregate: coll.collectionName,
|
|
1349
|
-
pipeline: input,
|
|
1350
|
-
cursor: {},
|
|
1351
|
-
...(snapshot && { readConcern: { level: 'snapshot' } }),
|
|
1352
|
-
};
|
|
1353
|
-
if (timeout !== null) {
|
|
1354
|
-
clearTimeout(timeout);
|
|
1355
|
-
timeout = null;
|
|
1356
|
-
}
|
|
1357
|
-
log('exec', streamName, req);
|
|
1358
|
-
const start2 = Date.now();
|
|
1359
|
-
return db.then(d => d.command(req)).then(result => {
|
|
1360
|
-
log('prepare', streamName, Date.now() - start);
|
|
1361
|
-
log('prepare2', streamName, start2 - start);
|
|
1362
|
-
const r = result;
|
|
1363
|
-
log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
|
|
1364
|
-
if (!state.steady) {
|
|
1365
|
-
if (timeout !== null)
|
|
1366
|
-
throw new Error('timeout should be null');
|
|
1367
|
-
timeout = setTimeout(() => {
|
|
1368
|
-
state.steady = true;
|
|
1369
|
-
console.log('steady');
|
|
1370
|
-
}, 10000);
|
|
1371
|
-
}
|
|
1372
|
-
return r;
|
|
1373
|
-
}, err => {
|
|
1374
|
-
log('err', req, err);
|
|
1375
|
-
throw new Error(err);
|
|
1376
|
-
});
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
1369
|
const firstWorksMerge = (iters) => {
|
|
1380
1370
|
const iterator = () => {
|
|
1381
1371
|
const results = iters.map(iter => iter());
|
|
1382
1372
|
const sources = { ...results };
|
|
1383
1373
|
return mergeIterators({
|
|
1384
1374
|
sources,
|
|
1385
|
-
interrupt: key => state.steady
|
|
1375
|
+
interrupt: key => state.steady,
|
|
1376
|
+
hooks: {
|
|
1377
|
+
start: (frame, result) => {
|
|
1378
|
+
if (!frame.info.job)
|
|
1379
|
+
return;
|
|
1380
|
+
if (timeout !== null) {
|
|
1381
|
+
clearTimeout(timeout);
|
|
1382
|
+
timeout = null;
|
|
1383
|
+
}
|
|
1384
|
+
result.next.then(() => {
|
|
1385
|
+
if (!frame.info.job)
|
|
1386
|
+
return;
|
|
1387
|
+
if (!state.steady) {
|
|
1388
|
+
if (timeout !== null)
|
|
1389
|
+
clearTimeout(timeout);
|
|
1390
|
+
timeout = setTimeout(() => {
|
|
1391
|
+
state.steady = true;
|
|
1392
|
+
console.log('steady');
|
|
1393
|
+
}, 2000);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1386
1398
|
});
|
|
1387
1399
|
};
|
|
1388
1400
|
return iterator;
|
|
@@ -1428,8 +1440,6 @@ const runCont = async (it, cb) => {
|
|
|
1428
1440
|
|
|
1429
1441
|
const merge = ({ lsource: L, rsource: R, }) => mergeIterators({ sources: { L, R } });
|
|
1430
1442
|
const join = ({ lField, rField, left, right, as }, leftSnapshot, rightSnapshot, stagesUntilNextLookup, outerLeft) => {
|
|
1431
|
-
createIndex(leftSnapshot.coll, { [`before.${lField.str()}`]: 1 }).catch(e => e.code == 86 || Promise.reject(e));
|
|
1432
|
-
createIndex(rightSnapshot.coll, { [`before.${rField.str()}`]: 1 }).catch(e => e.code == 86 || Promise.reject(e));
|
|
1433
1443
|
const rightJoinField = { field1: lField, field2: rField };
|
|
1434
1444
|
const joinId = 'left';
|
|
1435
1445
|
const joinR_Snapshot = asBefore($lookupRaw(rightJoinField, rightSnapshot, as, joinId, outerLeft));
|
|
@@ -1449,6 +1459,15 @@ const join = ({ lField, rField, left, right, as }, leftSnapshot, rightSnapshot,
|
|
|
1449
1459
|
const getRunner = (f, stages, final) => f.out({
|
|
1450
1460
|
raw: first => concatStages(stages, final.raw(first)),
|
|
1451
1461
|
teardown: final.teardown,
|
|
1462
|
+
}, async () => {
|
|
1463
|
+
log('Creating indexes for lookup left', leftSnapshot.coll.collectionName, {
|
|
1464
|
+
[`before.${lField.str()}`]: 1,
|
|
1465
|
+
});
|
|
1466
|
+
await createIndex(leftSnapshot.coll, { [`before.${lField.str()}`]: 1 }, { name: 'left_' + new mongodb.UUID().toString('base64') });
|
|
1467
|
+
log('Creating indexes for lookup right', rightSnapshot.coll.collectionName, {
|
|
1468
|
+
[`before.${rField.str()}`]: 1,
|
|
1469
|
+
});
|
|
1470
|
+
await createIndex(rightSnapshot.coll, { [`before.${rField.str()}`]: 1 }, { name: 'right_' + new mongodb.UUID().toString('base64') });
|
|
1452
1471
|
});
|
|
1453
1472
|
const lRunner = getRunner(left, lRunnerInput, finalInput);
|
|
1454
1473
|
const rRunner = getRunner(right, rRunnerInput, finalInput);
|
|
@@ -1512,10 +1531,7 @@ const $insertX = (out, expr, map, ext, extExpr) => {
|
|
|
1512
1531
|
teardown: c => c({
|
|
1513
1532
|
collection: out,
|
|
1514
1533
|
method: 'updateMany',
|
|
1515
|
-
params: [
|
|
1516
|
-
filter,
|
|
1517
|
-
[{ $set: { deletedAt: '$$NOW', touchedAt: '$$CLUSTER_TIME' } }],
|
|
1518
|
-
],
|
|
1534
|
+
params: [filter, [{ $set: { deletedAt: '$$NOW', touchedAt: '$$CLUSTER_TIME' } }]],
|
|
1519
1535
|
}),
|
|
1520
1536
|
raw: () => {
|
|
1521
1537
|
const replacer = map(mergeObjects(expr, field(mergeExpr(extExpr, {
|
|
@@ -1546,6 +1562,27 @@ const $insertPart = (out, ext) => {
|
|
|
1546
1562
|
const $insert = (out) => $insertPart(out, {});
|
|
1547
1563
|
const assertNotNull = (expr) => expr;
|
|
1548
1564
|
|
|
1565
|
+
const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
|
|
1566
|
+
const req = {
|
|
1567
|
+
aggregate: coll.collectionName,
|
|
1568
|
+
pipeline: input,
|
|
1569
|
+
cursor: {},
|
|
1570
|
+
...(snapshot && { readConcern: { level: 'snapshot' } }),
|
|
1571
|
+
};
|
|
1572
|
+
log('exec', streamName, req);
|
|
1573
|
+
const start2 = Date.now();
|
|
1574
|
+
return db.then(d => d.command(req)).then(result => {
|
|
1575
|
+
log('prepare', streamName, Date.now() - start);
|
|
1576
|
+
log('prepare2', streamName, start2 - start);
|
|
1577
|
+
const r = result;
|
|
1578
|
+
log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
|
|
1579
|
+
return r;
|
|
1580
|
+
}, err => {
|
|
1581
|
+
log('err', req, err);
|
|
1582
|
+
throw new Error(err);
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1549
1586
|
const addTeardown = (it, tr) => {
|
|
1550
1587
|
if (!tr)
|
|
1551
1588
|
return it;
|
|
@@ -1573,7 +1610,7 @@ async function getLastCommittedTs(adminDb) {
|
|
|
1573
1610
|
const st = await adminDb.command({ replSetGetStatus: 1 });
|
|
1574
1611
|
return st?.optimes?.lastCommittedOpTime?.ts ?? null;
|
|
1575
1612
|
}
|
|
1576
|
-
async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
|
|
1613
|
+
async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000 } = {}) {
|
|
1577
1614
|
const adminDb = db.client.db('admin');
|
|
1578
1615
|
const deadline = Date.now() + timeoutMs;
|
|
1579
1616
|
while (true) {
|
|
@@ -1581,7 +1618,7 @@ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
|
|
|
1581
1618
|
if (stable && stable.comp(oplogTs) >= 0)
|
|
1582
1619
|
return;
|
|
1583
1620
|
if (Date.now() > deadline) {
|
|
1584
|
-
throw new Error(
|
|
1621
|
+
throw new Error('Timed out waiting for stable timestamp to reach oplog event time');
|
|
1585
1622
|
}
|
|
1586
1623
|
await sleep(pollMs);
|
|
1587
1624
|
}
|
|
@@ -1603,29 +1640,37 @@ async function* tailOplog(db, opts) {
|
|
|
1603
1640
|
try {
|
|
1604
1641
|
for await (const doc of cursor) {
|
|
1605
1642
|
lastTs = doc.ts;
|
|
1606
|
-
if (doc.op === 'i') {
|
|
1607
|
-
|
|
1643
|
+
if (doc.op === 'i' || '_id' in doc.o) {
|
|
1644
|
+
const fields = new Set(Object.keys(doc.o));
|
|
1645
|
+
fields.delete('_id');
|
|
1646
|
+
yield { fields, doc, changeTouched: doc.o['touchedAt'] instanceof mongodb.MaxKey };
|
|
1608
1647
|
}
|
|
1609
1648
|
else {
|
|
1649
|
+
let changeTouched = false;
|
|
1610
1650
|
if (doc.o['$v'] !== 2) {
|
|
1611
|
-
throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc
|
|
1651
|
+
throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc)}`);
|
|
1612
1652
|
}
|
|
1613
1653
|
const updatedFields = [];
|
|
1614
1654
|
const diff = doc.o['diff'];
|
|
1615
1655
|
for (const updateOp in diff) {
|
|
1616
1656
|
if (['u', 'i', 'd'].includes(updateOp)) {
|
|
1617
1657
|
updatedFields.push(...Object.keys(diff[updateOp]));
|
|
1658
|
+
if (diff[updateOp]['touchedAt'] instanceof mongodb.MaxKey) {
|
|
1659
|
+
changeTouched = true;
|
|
1660
|
+
}
|
|
1618
1661
|
}
|
|
1619
1662
|
else if (updateOp.startsWith('s')) {
|
|
1620
1663
|
updatedFields.push(updateOp.slice(1));
|
|
1621
1664
|
}
|
|
1622
1665
|
}
|
|
1623
|
-
yield {
|
|
1666
|
+
yield { fields: new Set(updatedFields), doc, changeTouched };
|
|
1624
1667
|
}
|
|
1625
1668
|
}
|
|
1626
1669
|
}
|
|
1627
1670
|
catch (e) {
|
|
1628
|
-
log('oplog loop error
|
|
1671
|
+
log('oplog loop error, notifying watchers and reopening');
|
|
1672
|
+
console.error(e);
|
|
1673
|
+
yield null;
|
|
1629
1674
|
}
|
|
1630
1675
|
finally {
|
|
1631
1676
|
log('oplog loop ended');
|
|
@@ -1636,15 +1681,68 @@ async function* tailOplog(db, opts) {
|
|
|
1636
1681
|
}
|
|
1637
1682
|
const watchers = new Map();
|
|
1638
1683
|
let running = false;
|
|
1684
|
+
const makePromise = () => {
|
|
1685
|
+
let resolve = () => { };
|
|
1686
|
+
let promise = new Promise(r => (resolve = r));
|
|
1687
|
+
return { promise, resolve };
|
|
1688
|
+
};
|
|
1639
1689
|
const loop = async (db) => {
|
|
1640
1690
|
log('starting oplog loop');
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1691
|
+
let notify = makePromise();
|
|
1692
|
+
let batch = [];
|
|
1693
|
+
const run = async () => {
|
|
1694
|
+
for await (const event of tailOplog(db, {})) {
|
|
1695
|
+
if (event?.fields.size === 0)
|
|
1696
|
+
continue;
|
|
1697
|
+
batch = event && batch ? [...batch, event] : null;
|
|
1698
|
+
notify.resolve();
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
run();
|
|
1702
|
+
const iter = async function* () {
|
|
1703
|
+
while (true) {
|
|
1704
|
+
await notify.promise;
|
|
1705
|
+
const b = batch;
|
|
1706
|
+
batch = [];
|
|
1707
|
+
notify = makePromise();
|
|
1708
|
+
yield b;
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
for await (const events of iter()) {
|
|
1712
|
+
if (!events) {
|
|
1713
|
+
log('notifying watchers of oplog loop restart');
|
|
1714
|
+
for (const m of watchers.values()) {
|
|
1715
|
+
for (const { cb } of m.values()) {
|
|
1716
|
+
cb(null);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1644
1719
|
continue;
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1720
|
+
}
|
|
1721
|
+
const groups = Object.groupBy(events.filter(e => e.changeTouched), ev => ev.doc.ns);
|
|
1722
|
+
for (const [ns, evs] of Object.entries(groups)) {
|
|
1723
|
+
if (!evs)
|
|
1724
|
+
continue;
|
|
1725
|
+
const [dbName, collName] = ns.split('.');
|
|
1726
|
+
if (dbName !== db.databaseName)
|
|
1727
|
+
continue;
|
|
1728
|
+
const coll = db.collection(collName);
|
|
1729
|
+
coll
|
|
1730
|
+
.bulkWrite(evs.map((e) => ({
|
|
1731
|
+
updateOne: {
|
|
1732
|
+
filter: { _id: e.doc.o['_id'] ?? e.doc.o2?._id },
|
|
1733
|
+
update: { $set: { touchedAt: e.doc.ts } },
|
|
1734
|
+
},
|
|
1735
|
+
})))
|
|
1736
|
+
.catch(() => { });
|
|
1737
|
+
}
|
|
1738
|
+
for (const { fields, doc } of events) {
|
|
1739
|
+
const m = watchers.get(doc.ns);
|
|
1740
|
+
if (!m)
|
|
1741
|
+
continue;
|
|
1742
|
+
for (const { cb, keys } of m.values()) {
|
|
1743
|
+
if (!keys || keys.some(k => fields.has(k))) {
|
|
1744
|
+
cb(doc);
|
|
1745
|
+
}
|
|
1648
1746
|
}
|
|
1649
1747
|
}
|
|
1650
1748
|
}
|
|
@@ -1666,11 +1764,16 @@ const register = (coll, keys, cb) => {
|
|
|
1666
1764
|
watchers.delete(ns);
|
|
1667
1765
|
};
|
|
1668
1766
|
};
|
|
1669
|
-
|
|
1767
|
+
let maxKeysRemoved = null;
|
|
1768
|
+
const makeWatchStream = async ({ collection, projection: p, hardMatch: m }, streamName) => {
|
|
1769
|
+
const { db } = collection.s;
|
|
1770
|
+
await (maxKeysRemoved ??= Promise.all((await db.listCollections({}, { nameOnly: true }).toArray()).map(x => void db
|
|
1771
|
+
.collection(x.name)
|
|
1772
|
+
.updateMany({ touchedAt: new mongodb.MaxKey() }, [{ $set: { touchedAt: '$$CLUSTER_TIME' } }]))).then(() => { }));
|
|
1670
1773
|
const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
|
|
1671
1774
|
let resolve = (_) => { };
|
|
1672
1775
|
const promise = new Promise(r => (resolve = r));
|
|
1673
|
-
const close = register(collection, p ? Object.keys(projection) : null,
|
|
1776
|
+
const close = register(collection, p ? Object.keys(projection) : null, doc => {
|
|
1674
1777
|
log(streamName, 'change detected', doc);
|
|
1675
1778
|
resolve(doc);
|
|
1676
1779
|
close();
|
|
@@ -1679,9 +1782,10 @@ const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName
|
|
|
1679
1782
|
tryNext: async () => {
|
|
1680
1783
|
const doc = await promise;
|
|
1681
1784
|
const start = Date.now();
|
|
1682
|
-
|
|
1785
|
+
if (doc)
|
|
1786
|
+
await waitUntilStablePast(collection.s.db, doc.ts);
|
|
1683
1787
|
log(streamName, 'stable past took', Date.now() - start);
|
|
1684
|
-
return doc;
|
|
1788
|
+
return doc ?? {};
|
|
1685
1789
|
},
|
|
1686
1790
|
close: async () => close(),
|
|
1687
1791
|
};
|
|
@@ -1775,44 +1879,17 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1775
1879
|
streamNames[streamName] = hash;
|
|
1776
1880
|
else if (streamNames[streamName] != hash)
|
|
1777
1881
|
throw new Error(`streamName ${streamName} already used`);
|
|
1778
|
-
db.command({
|
|
1779
|
-
collMod: coll,
|
|
1780
|
-
changeStreamPreAndPostImages: { enabled: true },
|
|
1781
|
-
});
|
|
1782
|
-
createIndex(collection, { touchedAt: 1 }, hardMatch
|
|
1783
|
-
? {
|
|
1784
|
-
partialFilterExpression: hardMatch.raw(root()),
|
|
1785
|
-
name: 'touchedAt_hard_' + new mongodb.UUID().toString('base64'),
|
|
1786
|
-
}
|
|
1787
|
-
: {}).catch(e => e.code == 86 || Promise.reject(e));
|
|
1788
1882
|
const last = db.collection('__last');
|
|
1789
1883
|
const snapshotCollection = db.collection(coll + '_' + streamName + '_snapshot');
|
|
1790
|
-
createIndex(snapshotCollection, { before: 1 }, {
|
|
1791
|
-
partialFilterExpression: { before: null },
|
|
1792
|
-
name: 'before_' + new mongodb.UUID().toString('base64'),
|
|
1793
|
-
});
|
|
1794
|
-
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1795
|
-
partialFilterExpression: { updated: true },
|
|
1796
|
-
name: 'updated_' + new mongodb.UUID().toString('base64'),
|
|
1797
|
-
});
|
|
1798
|
-
createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1799
|
-
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1800
|
-
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
1801
|
-
});
|
|
1802
|
-
createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1803
|
-
partialFilterExpression: { updated: true, after: null },
|
|
1804
|
-
name: 'updated_no_after_' + new mongodb.UUID().toString('base64'),
|
|
1805
|
-
});
|
|
1806
|
-
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1807
|
-
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1808
|
-
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
1809
|
-
});
|
|
1810
1884
|
const job = {};
|
|
1811
|
-
const run = (finalInput) => {
|
|
1812
|
-
const
|
|
1813
|
-
snapshotCollection.drop().catch(noop)
|
|
1814
|
-
|
|
1815
|
-
|
|
1885
|
+
const run = (finalInput, setup) => {
|
|
1886
|
+
const dropSnapshot = async () => {
|
|
1887
|
+
await snapshotCollection.drop().catch(noop);
|
|
1888
|
+
log('snapshot collection dropped', streamName, `db['${snapshotCollection.collectionName}'].drop()`);
|
|
1889
|
+
log('with', [...(indexMap.get(snapshotCollection.collectionName)?.keys() ?? [])], 'indexes in map before deletion');
|
|
1890
|
+
indexMap.delete(snapshotCollection.collectionName);
|
|
1891
|
+
};
|
|
1892
|
+
const clear = async () => Promise.all([dropSnapshot(), last.deleteOne({ _id: streamName })]);
|
|
1816
1893
|
const withStop = (next, tr) => {
|
|
1817
1894
|
return addTeardown(() => ({ stop, next: next(), clear }), tr);
|
|
1818
1895
|
};
|
|
@@ -1837,7 +1914,9 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1837
1914
|
const step0 = () => synchronousPromise.SynchronousPromise.resolve(next(step1, 'empty new collection'));
|
|
1838
1915
|
const stop = withStop(step0);
|
|
1839
1916
|
const step1 = async () => {
|
|
1917
|
+
log('reset collection', streamName, `db['${snapshotCollection.collectionName}'].updateMany( updated: true }, { $set: { updated: false, after: null } })`);
|
|
1840
1918
|
await snapshotCollection.updateMany({ updated: true }, { $set: { updated: false, after: null } });
|
|
1919
|
+
log('reset collection done', streamName);
|
|
1841
1920
|
return next(step2, 'get last update');
|
|
1842
1921
|
};
|
|
1843
1922
|
const step2 = () => Promise.all([
|
|
@@ -1860,13 +1939,45 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1860
1939
|
};
|
|
1861
1940
|
const [action, out] = actions[method](collection, params);
|
|
1862
1941
|
log('teardown', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
|
|
1863
|
-
await Promise.all([
|
|
1942
|
+
await Promise.all([dropSnapshot(), action]);
|
|
1864
1943
|
log('teardown done', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
|
|
1865
1944
|
};
|
|
1866
1945
|
if (!same) {
|
|
1867
1946
|
log('not same, new data', streamName, data);
|
|
1868
1947
|
await handleTeardown(exists ?? { data });
|
|
1869
1948
|
}
|
|
1949
|
+
log('creating indexes');
|
|
1950
|
+
await createIndex(snapshotCollection, { before: 1 }, {
|
|
1951
|
+
partialFilterExpression: { before: null },
|
|
1952
|
+
name: 'before_' + new mongodb.UUID().toString('base64'),
|
|
1953
|
+
});
|
|
1954
|
+
await createIndex(snapshotCollection, { updated: 1 }, {
|
|
1955
|
+
partialFilterExpression: { updated: true },
|
|
1956
|
+
name: 'updated_' + new mongodb.UUID().toString('base64'),
|
|
1957
|
+
});
|
|
1958
|
+
await createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1959
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1960
|
+
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
1961
|
+
});
|
|
1962
|
+
await createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1963
|
+
partialFilterExpression: { updated: true, after: null },
|
|
1964
|
+
name: 'updated_no_after_' + new mongodb.UUID().toString('base64'),
|
|
1965
|
+
});
|
|
1966
|
+
await createIndex(snapshotCollection, { updated: 1 }, {
|
|
1967
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1968
|
+
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
1969
|
+
});
|
|
1970
|
+
await db.command({
|
|
1971
|
+
collMod: coll,
|
|
1972
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1973
|
+
});
|
|
1974
|
+
await createIndex(collection, { touchedAt: 1 }, hardMatch
|
|
1975
|
+
? {
|
|
1976
|
+
partialFilterExpression: hardMatch.raw(root()),
|
|
1977
|
+
name: 'touchedAt_hard_' + new mongodb.UUID().toString('base64'),
|
|
1978
|
+
}
|
|
1979
|
+
: {});
|
|
1980
|
+
await setup?.();
|
|
1870
1981
|
await after?.();
|
|
1871
1982
|
return nextData([])(async () => {
|
|
1872
1983
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -1906,7 +2017,8 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1906
2017
|
const first = ts === undefined;
|
|
1907
2018
|
const stages = finalInput.raw(first);
|
|
1908
2019
|
await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
|
|
1909
|
-
const stream = makeStream();
|
|
2020
|
+
const stream = await makeStream();
|
|
2021
|
+
const nextRes = stream.tryNext();
|
|
1910
2022
|
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1911
2023
|
coll: snapshotCollection,
|
|
1912
2024
|
input: link()
|
|
@@ -1920,7 +2032,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1920
2032
|
.with(input.delta)
|
|
1921
2033
|
.with(stages).stages,
|
|
1922
2034
|
}), false, start);
|
|
1923
|
-
const nextRes = stream.tryNext();
|
|
1924
2035
|
stages.at(-1).$merge.into.coll;
|
|
1925
2036
|
return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
|
|
1926
2037
|
};
|
|
@@ -1999,14 +2110,6 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1999
2110
|
: pre;
|
|
2000
2111
|
const job = {};
|
|
2001
2112
|
const db = collection.s.db, coll = collection.collectionName;
|
|
2002
|
-
db.command({
|
|
2003
|
-
collMod: coll,
|
|
2004
|
-
changeStreamPreAndPostImages: { enabled: true },
|
|
2005
|
-
});
|
|
2006
|
-
createIndex(collection, { touchedAt: 1 }, {
|
|
2007
|
-
partialFilterExpression: { deletedAt: { $eq: null } },
|
|
2008
|
-
name: 'touchedAt_' + new mongodb.UUID().toString('base64'),
|
|
2009
|
-
});
|
|
2010
2113
|
const last = db.collection('__last');
|
|
2011
2114
|
const projectInput = projection &&
|
|
2012
2115
|
$project_(spread(projection, {
|
|
@@ -2043,10 +2146,26 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
2043
2146
|
};
|
|
2044
2147
|
const step0 = () => synchronousPromise.SynchronousPromise.resolve(next(step1, 'get last update'));
|
|
2045
2148
|
const stop = withStop(step0);
|
|
2046
|
-
const step1 = () =>
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2149
|
+
const step1 = async () => {
|
|
2150
|
+
log('creating indexes');
|
|
2151
|
+
await db.command({
|
|
2152
|
+
collMod: coll,
|
|
2153
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
2154
|
+
});
|
|
2155
|
+
await createIndex(collection, { touchedAt: 1 }, {
|
|
2156
|
+
partialFilterExpression: { deletedAt: { $eq: null } },
|
|
2157
|
+
name: 'touchedAt_' + new mongodb.UUID().toString('base64'),
|
|
2158
|
+
});
|
|
2159
|
+
log('start stream', { streamName, data });
|
|
2160
|
+
await last.findOne();
|
|
2161
|
+
console.log('got last update');
|
|
2162
|
+
const p = last.findOne({ _id: streamName, data });
|
|
2163
|
+
await p;
|
|
2164
|
+
log('stream started', { streamName, data });
|
|
2165
|
+
const ts = await Promise.all([p, last.findOne({ _id: streamName })]);
|
|
2166
|
+
log('got last update', { streamName, ts });
|
|
2167
|
+
return next(step2_5(ts), 'handle teardown');
|
|
2168
|
+
};
|
|
2050
2169
|
const step2_5 = ([same, exists]) => async () => {
|
|
2051
2170
|
const handleTeardown = async (last) => {
|
|
2052
2171
|
if (!last.data)
|
|
@@ -2076,7 +2195,7 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
2076
2195
|
const makeStream = () => makeWatchStream(view, streamName);
|
|
2077
2196
|
const step4 = (lastTS) => async () => {
|
|
2078
2197
|
const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
|
|
2079
|
-
const stream = makeStream();
|
|
2198
|
+
const stream = await makeStream();
|
|
2080
2199
|
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
2081
2200
|
coll: collection,
|
|
2082
2201
|
input: raw,
|
|
@@ -2245,7 +2364,6 @@ exports.range = range;
|
|
|
2245
2364
|
exports.regex = regex;
|
|
2246
2365
|
exports.root = root;
|
|
2247
2366
|
exports.set = set;
|
|
2248
|
-
exports.setF = setF;
|
|
2249
2367
|
exports.setField = setField;
|
|
2250
2368
|
exports.single = single;
|
|
2251
2369
|
exports.size = size;
|