@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.
Files changed (4) hide show
  1. package/index.d.ts +7 -10
  2. package/index.esm.js +242 -123
  3. package/index.js +241 -123
  4. 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
- }, indexSpec: IndexSpecification, options?: CreateIndexesOptions) => Promise<void>;
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" | "$gt" | "$lte" | "$gte") => <T extends Numeric>(operand: rawItem & D2) => Predicate<D2>;
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, setF, setField, single, size, slice, sortArray, staging, startOf, str, sub, subtract, to, toInt, val, weekPart, wrap, year };
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('$$CLUSTER_TIME'),
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 createIndex = async (collection, indexSpec, options) => {
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 T = (s) => `Timestamp(${parseInt(`${BigInt(s) / 2n ** 32n}`)}, ${parseInt(`${BigInt(s) % 2n ** 32n}`)})`;
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("Timed out waiting for stable timestamp to reach oplog event time");
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
- yield { ns: doc.ns, fields: new Set(Object.keys(doc.o)), doc };
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.o)}`);
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 { ns: doc.ns, fields: new Set(updatedFields), doc };
1664
+ yield { fields: new Set(updatedFields), doc, changeTouched };
1622
1665
  }
1623
1666
  }
1624
1667
  }
1625
1668
  catch (e) {
1626
- log('oplog loop error', e);
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
- for await (const { ns, fields, doc } of tailOplog(db, {})) {
1640
- const m = watchers.get(ns);
1641
- if (!m)
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
- for (const { cb, keys } of m.values()) {
1644
- if (!keys || keys.some(k => fields.has(k))) {
1645
- cb(doc);
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
- const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
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, (doc) => {
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
- await waitUntilStablePast(collection.s.db, doc.ts);
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 clear = async () => Promise.all([
1811
- snapshotCollection.drop().catch(noop).catch(noop),
1812
- last.deleteOne({ _id: streamName }),
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([snapshotCollection.drop().catch(noop), action]);
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 = () => Promise.all([
2045
- last.findOne({ _id: streamName, data }),
2046
- last.findOne({ _id: streamName }),
2047
- ]).then(ts => next(step2_5(ts), 'handle teardown'));
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, setF, setField, single, size, slice, sortArray, staging, startOf, str, sub, subtract, to, toInt, val, weekPart, wrap, year };
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('$$CLUSTER_TIME'),
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 createIndex = async (collection, indexSpec, options) => {
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 T = (s) => `Timestamp(${parseInt(`${BigInt(s) / 2n ** 32n}`)}, ${parseInt(`${BigInt(s) % 2n ** 32n}`)})`;
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("Timed out waiting for stable timestamp to reach oplog event time");
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
- yield { ns: doc.ns, fields: new Set(Object.keys(doc.o)), doc };
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.o)}`);
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 { ns: doc.ns, fields: new Set(updatedFields), doc };
1666
+ yield { fields: new Set(updatedFields), doc, changeTouched };
1624
1667
  }
1625
1668
  }
1626
1669
  }
1627
1670
  catch (e) {
1628
- log('oplog loop error', e);
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
- for await (const { ns, fields, doc } of tailOplog(db, {})) {
1642
- const m = watchers.get(ns);
1643
- if (!m)
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
- for (const { cb, keys } of m.values()) {
1646
- if (!keys || keys.some(k => fields.has(k))) {
1647
- cb(doc);
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
- const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
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, (doc) => {
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
- await waitUntilStablePast(collection.s.db, doc.ts);
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 clear = async () => Promise.all([
1813
- snapshotCollection.drop().catch(noop).catch(noop),
1814
- last.deleteOne({ _id: streamName }),
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([snapshotCollection.drop().catch(noop), action]);
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 = () => Promise.all([
2047
- last.findOne({ _id: streamName, data }),
2048
- last.findOne({ _id: streamName }),
2049
- ]).then(ts => next(step2_5(ts), 'handle teardown'));
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;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "index.esm.js",
4
4
  "typings": "index.d.ts",
5
5
  "name": "@omegup/msync",
6
- "version": "0.1.21",
6
+ "version": "0.1.23",
7
7
  "dependencies": {
8
8
  "dayjs": "^1.11.9",
9
9
  "dotenv": "^16.3.1",