@omegup/msync 0.1.19 → 0.1.21

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 +5 -1
  2. package/index.esm.js +230 -140
  3. package/index.js +229 -138
  4. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -730,8 +730,12 @@ declare const $and: Combiner;
730
730
  declare const $nor: Combiner;
731
731
  declare const $or: Combiner;
732
732
 
733
+ declare const setF: (f: ({ input }: {
734
+ input: any;
735
+ }) => Promise<void>) => void;
736
+
733
737
  declare const enablePreAndPostImages: <T extends doc>(coll: Collection<T>) => Promise<Document>;
734
738
  declare const prepare: (testName?: string) => Promise<MongoClient$1>;
735
739
  declare const makeCol: <T extends ID>(docs: readonly OptionalUnlessRequiredId<T>[], database: Db, name?: string) => Promise<Collection<T>>;
736
740
 
737
- 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 };
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 };
package/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import crypto$1 from 'crypto';
2
2
  import { canonicalize } from 'json-canonicalize';
3
3
  import { SynchronousPromise } from 'synchronous-promise';
4
- import { UUID, MongoClient } from 'mongodb';
4
+ import { MongoClient, UUID } from 'mongodb';
5
5
  import { writeFile } from 'fs/promises';
6
6
 
7
7
  const asExprRaw = (raw) => ({ get: () => raw });
@@ -1336,9 +1336,12 @@ const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
1336
1336
  const json = (a) => replace(JSON.stringify(a));
1337
1337
  const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
1338
1338
 
1339
- const state = { steady: false };
1339
+ const state = { steady: false, f: (_) => Promise.resolve() };
1340
1340
  let timeout = null;
1341
- const aggregate = (streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
1341
+ const setF = (f) => {
1342
+ state.f = f;
1343
+ };
1344
+ const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
1342
1345
  const req = {
1343
1346
  aggregate: coll.collectionName,
1344
1347
  pipeline: input,
@@ -1350,9 +1353,12 @@ const aggregate = (streamName, input, snapshot = true, start = Date.now()) => in
1350
1353
  timeout = null;
1351
1354
  }
1352
1355
  log('exec', streamName, req);
1353
- return coll.s.db.command(req).then(result => {
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);
1354
1360
  const r = result;
1355
- log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('$$CLUSTER_TIME', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
1361
+ log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
1356
1362
  if (!state.steady) {
1357
1363
  if (timeout !== null)
1358
1364
  throw new Error('timeout should be null');
@@ -1555,67 +1561,128 @@ const addTeardown = (it, tr) => {
1555
1561
  };
1556
1562
  };
1557
1563
 
1558
- const changeKeys = ['fullDocument', 'fullDocumentBeforeChange'];
1559
- const subQ = (a, f) => ({ raw: g => a.raw(g.with(f)) });
1560
- const makeWatchStream = (db, { collection, projection: p, hardMatch: m }, startAt, streamName) => {
1561
- const projection = p ? { ...mapExactToObject(p, v => v), deletedAt: 1 } : 1;
1562
- const pipeline = [];
1563
- if (m) {
1564
- const q = $or(...changeKeys.map((k) => subQ(m, root().of(k))));
1565
- if (q)
1566
- pipeline.push({
1567
- $match: {
1568
- $or: [
1569
- q.raw(root()),
1570
- Object.fromEntries(changeKeys.map(k => [k, null])),
1571
- ],
1572
- },
1573
- });
1564
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
1565
+ const getCurrentTimestamp = async (db) => {
1566
+ const adminDb = db.admin();
1567
+ const serverStatus = await adminDb.command({ serverStatus: 1 });
1568
+ return serverStatus['operationTime'];
1569
+ };
1570
+ async function getLastCommittedTs(adminDb) {
1571
+ const st = await adminDb.command({ replSetGetStatus: 1 });
1572
+ return st?.optimes?.lastCommittedOpTime?.ts ?? null;
1573
+ }
1574
+ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000, } = {}) {
1575
+ const adminDb = db.client.db('admin');
1576
+ const deadline = Date.now() + timeoutMs;
1577
+ while (true) {
1578
+ const stable = await getLastCommittedTs(adminDb);
1579
+ if (stable && stable.comp(oplogTs) >= 0)
1580
+ return;
1581
+ if (Date.now() > deadline) {
1582
+ throw new Error("Timed out waiting for stable timestamp to reach oplog event time");
1583
+ }
1584
+ await sleep(pollMs);
1574
1585
  }
1575
- pipeline.push({
1576
- $project: {
1577
- _id: 1,
1578
- fullDocument: projection,
1579
- fullDocumentBeforeChange: projection,
1580
- documentKey: 1,
1581
- clusterTime: 1,
1582
- },
1583
- });
1584
- pipeline.push({
1585
- $match: {
1586
- clusterTime: { $gt: startAt },
1587
- $or: [
1588
- {
1589
- $expr: {
1590
- $ne: [
1591
- { $mergeObjects: ['$fullDocument', { touchedAt: null }] },
1592
- { $mergeObjects: ['$fullDocumentBeforeChange', { touchedAt: null }] },
1593
- ],
1594
- },
1595
- },
1596
- Object.fromEntries(changeKeys.map(k => [k, null])),
1597
- ],
1598
- },
1586
+ }
1587
+ async function* tailOplog(db, opts) {
1588
+ let lastTs = opts.since ?? (await getCurrentTimestamp(db));
1589
+ const reopenDelayMs = opts.reopenDelayMs ?? 250;
1590
+ const coll = db.client.db('local').collection('oplog.rs');
1591
+ while (true) {
1592
+ const cursor = coll.find({
1593
+ ts: { $gt: lastTs },
1594
+ ns: RegExp(`^${db.namespace}\\.(?!tmp_)(?!__).*(?<!_snapshot)$`),
1595
+ op: { $in: ['i', 'u'] },
1596
+ }, {
1597
+ tailable: true,
1598
+ awaitData: true,
1599
+ noCursorTimeout: true,
1600
+ });
1601
+ try {
1602
+ for await (const doc of cursor) {
1603
+ lastTs = doc.ts;
1604
+ if (doc.op === 'i') {
1605
+ yield { ns: doc.ns, fields: new Set(Object.keys(doc.o)), doc };
1606
+ }
1607
+ else {
1608
+ if (doc.o['$v'] !== 2) {
1609
+ throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc.o)}`);
1610
+ }
1611
+ const updatedFields = [];
1612
+ const diff = doc.o['diff'];
1613
+ for (const updateOp in diff) {
1614
+ if (['u', 'i', 'd'].includes(updateOp)) {
1615
+ updatedFields.push(...Object.keys(diff[updateOp]));
1616
+ }
1617
+ else if (updateOp.startsWith('s')) {
1618
+ updatedFields.push(updateOp.slice(1));
1619
+ }
1620
+ }
1621
+ yield { ns: doc.ns, fields: new Set(updatedFields), doc };
1622
+ }
1623
+ }
1624
+ }
1625
+ catch (e) {
1626
+ log('oplog loop error', e);
1627
+ }
1628
+ finally {
1629
+ log('oplog loop ended');
1630
+ await cursor.close().catch(() => { });
1631
+ }
1632
+ await sleep(reopenDelayMs);
1633
+ }
1634
+ }
1635
+ const watchers = new Map();
1636
+ let running = false;
1637
+ const loop = async (db) => {
1638
+ log('starting oplog loop');
1639
+ for await (const { ns, fields, doc } of tailOplog(db, {})) {
1640
+ const m = watchers.get(ns);
1641
+ if (!m)
1642
+ continue;
1643
+ for (const { cb, keys } of m.values()) {
1644
+ if (!keys || keys.some(k => fields.has(k))) {
1645
+ cb(doc);
1646
+ }
1647
+ }
1648
+ }
1649
+ };
1650
+ const register = (coll, keys, cb) => {
1651
+ const ns = coll.namespace;
1652
+ let m = watchers.get(ns);
1653
+ if (!m)
1654
+ watchers.set(ns, (m = new Map()));
1655
+ const id = crypto.randomUUID();
1656
+ m.set(id, { cb, keys });
1657
+ if (!running) {
1658
+ running = true;
1659
+ loop(coll.s.db);
1660
+ }
1661
+ return () => {
1662
+ m.delete(id);
1663
+ if (m.size === 0)
1664
+ watchers.delete(ns);
1665
+ };
1666
+ };
1667
+ const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
1668
+ const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
1669
+ let resolve = (_) => { };
1670
+ const promise = new Promise(r => (resolve = r));
1671
+ const close = register(collection, p ? Object.keys(projection) : null, (doc) => {
1672
+ log(streamName, 'change detected', doc);
1673
+ resolve(doc);
1674
+ close();
1599
1675
  });
1600
- pipeline.push({
1601
- $project: {
1602
- _id: 1,
1676
+ return {
1677
+ tryNext: async () => {
1678
+ const doc = await promise;
1679
+ const start = Date.now();
1680
+ await waitUntilStablePast(collection.s.db, doc.ts);
1681
+ log(streamName, 'stable past took', Date.now() - start);
1682
+ return doc;
1603
1683
  },
1604
- });
1605
- const stream = db.collection(collection.collectionName).watch(pipeline, {
1606
- fullDocument: 'required',
1607
- fullDocumentBeforeChange: 'required',
1608
- startAtOperationTime: startAt,
1609
- });
1610
- const tryNext = async () => {
1611
- const doc = await stream.tryNext();
1612
- if (doc)
1613
- await new Promise(resolve => setTimeout(resolve, 100));
1614
- if (doc)
1615
- log('detected', streamName, collection.collectionName, doc);
1616
- return doc;
1684
+ close: async () => close(),
1617
1685
  };
1618
- return { tryNext, close: () => stream.close() };
1619
1686
  };
1620
1687
 
1621
1688
  const actions = {
@@ -1648,10 +1715,54 @@ const getFirstStages = (view, needs) => {
1648
1715
  return { firstStages, hardMatch };
1649
1716
  };
1650
1717
 
1651
- const tryNext$1 = (stream) => stream.tryNext().catch(() => ({}));
1718
+ require('dotenv').config();
1719
+ const uri = process.env['MONGO_URL'];
1720
+
1721
+ const enablePreAndPostImages = (coll) => coll.s.db.command({
1722
+ collMod: coll.collectionName,
1723
+ changeStreamPreAndPostImages: { enabled: true },
1724
+ });
1725
+ const prepare = async (testName) => {
1726
+ const client = new MongoClient(uri, testName ? { monitorCommands: true } : {});
1727
+ if (testName) {
1728
+ const handler = (c) => {
1729
+ writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
1730
+ };
1731
+ client.on('commandStarted', handler);
1732
+ client.on('commandSucceeded', handler);
1733
+ }
1734
+ await client.connect();
1735
+ await client.db('admin').command({
1736
+ setClusterParameter: {
1737
+ changeStreamOptions: {
1738
+ preAndPostImages: { expireAfterSeconds: 60 },
1739
+ },
1740
+ },
1741
+ });
1742
+ return client;
1743
+ };
1744
+ const makeCol = async (docs, database, name) => {
1745
+ if (!name) {
1746
+ (name = crypto.randomUUID());
1747
+ }
1748
+ try {
1749
+ const col = await database.createCollection(name, {
1750
+ changeStreamPreAndPostImages: { enabled: true },
1751
+ });
1752
+ if (docs.length)
1753
+ await col.insertMany([...docs]);
1754
+ return col;
1755
+ }
1756
+ catch {
1757
+ return database.collection(name);
1758
+ }
1759
+ };
1760
+
1652
1761
  const streamNames = {};
1653
1762
  const executes$2 = (view, input, streamName, skip = false, after, needs = {}) => {
1654
1763
  const { collection, projection, match } = view;
1764
+ const client = prepare();
1765
+ const pdb = client.then(cl => cl.db(collection.dbName));
1655
1766
  const { firstStages, hardMatch } = getFirstStages(view, needs);
1656
1767
  const db = collection.s.db, coll = collection.collectionName;
1657
1768
  const hash = crypto$1
@@ -1674,10 +1785,22 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1674
1785
  : {}).catch(e => e.code == 86 || Promise.reject(e));
1675
1786
  const last = db.collection('__last');
1676
1787
  const snapshotCollection = db.collection(coll + '_' + streamName + '_snapshot');
1788
+ createIndex(snapshotCollection, { before: 1 }, {
1789
+ partialFilterExpression: { before: null },
1790
+ name: 'before_' + new UUID().toString('base64'),
1791
+ });
1677
1792
  createIndex(snapshotCollection, { updated: 1 }, {
1678
1793
  partialFilterExpression: { updated: true },
1679
1794
  name: 'updated_' + new UUID().toString('base64'),
1680
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
+ });
1681
1804
  createIndex(snapshotCollection, { updated: 1 }, {
1682
1805
  partialFilterExpression: { updated: true, after: null, before: null },
1683
1806
  name: 'updated_nulls_' + new UUID().toString('base64'),
@@ -1716,7 +1839,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1716
1839
  return next(step2, 'get last update');
1717
1840
  };
1718
1841
  const step2 = () => Promise.all([
1719
- last.findOne({ _id: streamName, data }),
1842
+ last.findOne({ _id: streamName, data, job: null }),
1720
1843
  last.findOne({ _id: streamName }),
1721
1844
  ]).then(ts => next(step2_5(ts), ts[0]
1722
1845
  ? `no teardown to handle, starting at ${ts[0].ts}`
@@ -1739,7 +1862,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1739
1862
  log('teardown done', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
1740
1863
  };
1741
1864
  if (!same) {
1742
- log('not same, new data', data);
1865
+ log('not same, new data', streamName, data);
1743
1866
  await handleTeardown(exists ?? { data });
1744
1867
  }
1745
1868
  await after?.();
@@ -1768,40 +1891,46 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1768
1891
  whenMatched: link().with($replaceWith_(ite(eq(root().of('before').expr())(ctx()('new').of('after').expr()), root().expr(), mergeObjects(root().expr(), ctx()('new').expr())))).stages,
1769
1892
  whenNotMatched: 'insert',
1770
1893
  })).stages;
1771
- const r = await aggregate(streamName, c => c({ coll: collection, input: cloneIntoNew }));
1772
- await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
1894
+ const r = await aggregate(pdb, streamName, c => c({ coll: collection, input: cloneIntoNew }));
1895
+ const start = Date.now();
1896
+ const res = await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
1897
+ log('deleting from cloned into new collection', Date.now() - start, res, `db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null, before: null })`);
1773
1898
  return next(step4({ result: r, ts: lastTS?.ts }), 'run the aggregation');
1774
1899
  };
1775
- const makeStream = (startAt) => makeWatchStream(db, view, startAt, streamName);
1900
+ const makeStream = () => makeWatchStream(view, streamName);
1776
1901
  const step4 = ({ result, ts }) => async () => {
1777
1902
  const start = Date.now();
1778
- await snapshotCollection.updateMany({ before: null }, { $set: { before: null } });
1779
- const stages = finalInput.raw(ts === undefined);
1780
- const aggResult = await aggregate(streamName, c => c({
1903
+ log('snapshot', streamName, 'ensure before null', Date.now() - start);
1904
+ const first = ts === undefined;
1905
+ const stages = finalInput.raw(first);
1906
+ await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
1907
+ const stream = makeStream();
1908
+ const aggResult = await aggregate(pdb, streamName, c => c({
1781
1909
  coll: snapshotCollection,
1782
1910
  input: link()
1783
1911
  .with($match_(root().of('updated').has($eq(true))))
1912
+ .with($set_(set()({
1913
+ before: [
1914
+ 'before',
1915
+ to($ifNull(root().of('before').expr(), nil)),
1916
+ ],
1917
+ })))
1784
1918
  .with(input.delta)
1785
1919
  .with(stages).stages,
1786
1920
  }), false, start);
1787
- const stream = makeStream(result.cursor.atClusterTime);
1788
- const nextRes = tryNext$1(stream);
1789
- const intoColl = stages.at(-1).$merge.into.coll;
1790
- const startx = Date.now();
1791
- await db
1792
- .collection(intoColl)
1793
- .countDocuments({ touchedAt: { $gte: result.cursor.atClusterTime } })
1794
- .then(count => log(`documents updated ${intoColl}`, count, 'took', Date.now() - startx));
1795
- return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes }), 'remove handled deleted updated', () => stream.close());
1921
+ const nextRes = stream.tryNext();
1922
+ stages.at(-1).$merge.into.coll;
1923
+ return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
1796
1924
  };
1797
1925
  const step5 = (l) => async () => {
1798
- log(`remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
1926
+ log(streamName, `remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
1799
1927
  await snapshotCollection.deleteMany({ updated: true, after: null });
1800
1928
  log('removed handled deleted updated');
1801
1929
  return next(step6(l), 'update snapshot aggregation');
1802
1930
  };
1803
1931
  const step6 = (l) => async () => {
1804
1932
  log('update snapshot aggregation', `db['${snapshotCollection.collectionName}'].updateMany({ updated: true }, [ { $set: { updated: false, after: null, before: '$after' } } ])`);
1933
+ const start = Date.now();
1805
1934
  await snapshotCollection.updateMany({ updated: true }, [
1806
1935
  {
1807
1936
  $set: {
@@ -1811,23 +1940,27 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1811
1940
  },
1812
1941
  },
1813
1942
  ]);
1814
- log('updated snapshot aggregation');
1943
+ log('updated snapshot aggregation', Date.now() - start);
1815
1944
  return next(step7(l), 'update __last');
1816
1945
  };
1817
1946
  const step7 = (l) => async () => {
1818
- await last.updateOne({ _id: streamName }, {
1947
+ const start = Date.now();
1948
+ const patch = {
1819
1949
  $set: {
1820
1950
  ts: l.ts,
1821
- data,
1951
+ job: null,
1822
1952
  },
1823
- }, { upsert: true });
1953
+ };
1954
+ if (l.first)
1955
+ patch.$set = { ...patch.$set, data };
1956
+ await last.updateOne({ _id: streamName }, patch, { upsert: true });
1957
+ log('updated __last', Date.now() - start, `db['${last.collectionName}'].updateOne({ _id: '${streamName}' }, `, patch, `, { upsert: true })`);
1824
1958
  return step8(l);
1825
1959
  };
1826
1960
  const step8 = (l) => {
1827
- return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes
1828
- .then(doc => doc
1961
+ return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes.then(doc => doc
1829
1962
  ? next(step3({ _id: streamName, ts: l.ts }), 'restart')
1830
- : step8({ ...l, nextRes: tryNext$1(l.stream) })), 'wait for change');
1963
+ : step8({ ...l, nextRes: l.stream.tryNext() })), 'wait for change');
1831
1964
  };
1832
1965
  return skip
1833
1966
  ? withStop(() => SynchronousPromise.resolve(next(step3(null), 'clone into new collection')))
@@ -1845,7 +1978,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1845
1978
  };
1846
1979
  const staging = (view, streamName, skip = false, after) => pipe(input => executes$2(view, input, streamName, skip, after, view.needs), emptyDelta(), concatDelta, emptyDelta);
1847
1980
 
1848
- const tryNext = (stream) => stream.tryNext().catch(() => ({}));
1849
1981
  const executes$1 = (view, input, streamName, needs) => {
1850
1982
  const hash = crypto$1
1851
1983
  .createHash('md5')
@@ -1856,6 +1988,8 @@ const executes$1 = (view, input, streamName, needs) => {
1856
1988
  else if (streamNames[streamName] != hash)
1857
1989
  throw new Error('streamName already used');
1858
1990
  const { collection, projection, hardMatch: pre, match } = view;
1991
+ const client = prepare();
1992
+ const pdb = client.then(cl => cl.db(collection.dbName));
1859
1993
  const removeNotYetSynchronizedFields = projection &&
1860
1994
  Object.values(mapExactToObject(projection, (_, k) => (needs[k] ?? k.startsWith('_')) ? root().of(k).has($exists(true)) : null));
1861
1995
  const hardMatch = removeNotYetSynchronizedFields
@@ -1937,15 +2071,15 @@ const executes$1 = (view, input, streamName, needs) => {
1937
2071
  info: { debug: 'wait for clone into new collection', job: undefined },
1938
2072
  };
1939
2073
  };
1940
- const makeStream = (startAt) => makeWatchStream(db, view, startAt, streamName);
2074
+ const makeStream = () => makeWatchStream(view, streamName);
1941
2075
  const step4 = (lastTS) => async () => {
1942
2076
  const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
1943
- const aggResult = await aggregate(streamName, c => c({
2077
+ const stream = makeStream();
2078
+ const aggResult = await aggregate(pdb, streamName, c => c({
1944
2079
  coll: collection,
1945
2080
  input: raw,
1946
2081
  }));
1947
- const stream = makeStream(aggResult.cursor.atClusterTime);
1948
- const nextRes = tryNext(stream);
2082
+ const nextRes = stream.tryNext();
1949
2083
  return next(step7({ aggResult, ts: aggResult.cursor.atClusterTime, stream, nextRes }), 'update __last', () => stream.close());
1950
2084
  };
1951
2085
  const step7 = (l) => async () => {
@@ -1956,10 +2090,9 @@ const executes$1 = (view, input, streamName, needs) => {
1956
2090
  return {
1957
2091
  data: l.aggResult.cursor.firstBatch,
1958
2092
  info: { job: undefined, debug: 'wait for change' },
1959
- cont: withStop(() => l.nextRes
1960
- .then(doc => doc
2093
+ cont: withStop(() => l.nextRes.then(doc => doc
1961
2094
  ? next(step4({ _id: streamName, ts: l.ts }), 'restart')
1962
- : step8({ ...l, nextRes: tryNext(l.stream) }))),
2095
+ : step8({ ...l, nextRes: l.stream.tryNext() }))),
1963
2096
  };
1964
2097
  };
1965
2098
  return stop;
@@ -1986,47 +2119,4 @@ const executes = (view, input, needs) => {
1986
2119
  };
1987
2120
  const single = (view, needs = {}) => pipe(input => executes(view, input, needs), emptyDelta(), concatDelta, emptyDelta);
1988
2121
 
1989
- require('dotenv').config();
1990
- const uri = process.env['MONGO_URL'];
1991
-
1992
- const enablePreAndPostImages = (coll) => coll.s.db.command({
1993
- collMod: coll.collectionName,
1994
- changeStreamPreAndPostImages: { enabled: true },
1995
- });
1996
- const prepare = async (testName) => {
1997
- const client = new MongoClient(uri, testName ? { monitorCommands: true } : {});
1998
- if (testName) {
1999
- const handler = (c) => {
2000
- writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
2001
- };
2002
- client.on('commandStarted', handler);
2003
- client.on('commandSucceeded', handler);
2004
- }
2005
- await client.connect();
2006
- await client.db('admin').command({
2007
- setClusterParameter: {
2008
- changeStreamOptions: {
2009
- preAndPostImages: { expireAfterSeconds: 60 },
2010
- },
2011
- },
2012
- });
2013
- return client;
2014
- };
2015
- const makeCol = async (docs, database, name) => {
2016
- if (!name) {
2017
- (name = crypto.randomUUID());
2018
- }
2019
- try {
2020
- const col = await database.createCollection(name, {
2021
- changeStreamPreAndPostImages: { enabled: true },
2022
- });
2023
- if (docs.length)
2024
- await col.insertMany([...docs]);
2025
- return col;
2026
- }
2027
- catch {
2028
- return database.collection(name);
2029
- }
2030
- };
2031
-
2032
- 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 };
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 };
package/index.js CHANGED
@@ -1338,9 +1338,12 @@ const replace = (s) => s.replace(/\{"\$timestamp":"(\d+)"\}/g, (_, d) => T(d));
1338
1338
  const json = (a) => replace(JSON.stringify(a));
1339
1339
  const log = (...args) => console.log(new Date(), ...args.map(a => (typeof a === 'function' ? a(replace) : a && typeof a === 'object' ? json(a) : a)));
1340
1340
 
1341
- const state = { steady: false };
1341
+ const state = { steady: false, f: (_) => Promise.resolve() };
1342
1342
  let timeout = null;
1343
- const aggregate = (streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
1343
+ const setF = (f) => {
1344
+ state.f = f;
1345
+ };
1346
+ const aggregate = (db, streamName, input, snapshot = true, start = Date.now()) => input(({ coll, input }) => {
1344
1347
  const req = {
1345
1348
  aggregate: coll.collectionName,
1346
1349
  pipeline: input,
@@ -1352,9 +1355,12 @@ const aggregate = (streamName, input, snapshot = true, start = Date.now()) => in
1352
1355
  timeout = null;
1353
1356
  }
1354
1357
  log('exec', streamName, req);
1355
- return coll.s.db.command(req).then(result => {
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);
1356
1362
  const r = result;
1357
- log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('$$CLUSTER_TIME', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
1363
+ log('execed', streamName, (replace) => replace(JSON.stringify(req).replaceAll('"$$CLUSTER_TIME"', JSON.stringify(r.cursor.atClusterTime))), result, 'took', Date.now() - start);
1358
1364
  if (!state.steady) {
1359
1365
  if (timeout !== null)
1360
1366
  throw new Error('timeout should be null');
@@ -1557,67 +1563,128 @@ const addTeardown = (it, tr) => {
1557
1563
  };
1558
1564
  };
1559
1565
 
1560
- const changeKeys = ['fullDocument', 'fullDocumentBeforeChange'];
1561
- const subQ = (a, f) => ({ raw: g => a.raw(g.with(f)) });
1562
- const makeWatchStream = (db, { collection, projection: p, hardMatch: m }, startAt, streamName) => {
1563
- const projection = p ? { ...mapExactToObject(p, v => v), deletedAt: 1 } : 1;
1564
- const pipeline = [];
1565
- if (m) {
1566
- const q = $or(...changeKeys.map((k) => subQ(m, root().of(k))));
1567
- if (q)
1568
- pipeline.push({
1569
- $match: {
1570
- $or: [
1571
- q.raw(root()),
1572
- Object.fromEntries(changeKeys.map(k => [k, null])),
1573
- ],
1574
- },
1575
- });
1566
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
1567
+ const getCurrentTimestamp = async (db) => {
1568
+ const adminDb = db.admin();
1569
+ const serverStatus = await adminDb.command({ serverStatus: 1 });
1570
+ return serverStatus['operationTime'];
1571
+ };
1572
+ async function getLastCommittedTs(adminDb) {
1573
+ const st = await adminDb.command({ replSetGetStatus: 1 });
1574
+ return st?.optimes?.lastCommittedOpTime?.ts ?? null;
1575
+ }
1576
+ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000, } = {}) {
1577
+ const adminDb = db.client.db('admin');
1578
+ const deadline = Date.now() + timeoutMs;
1579
+ while (true) {
1580
+ const stable = await getLastCommittedTs(adminDb);
1581
+ if (stable && stable.comp(oplogTs) >= 0)
1582
+ return;
1583
+ if (Date.now() > deadline) {
1584
+ throw new Error("Timed out waiting for stable timestamp to reach oplog event time");
1585
+ }
1586
+ await sleep(pollMs);
1576
1587
  }
1577
- pipeline.push({
1578
- $project: {
1579
- _id: 1,
1580
- fullDocument: projection,
1581
- fullDocumentBeforeChange: projection,
1582
- documentKey: 1,
1583
- clusterTime: 1,
1584
- },
1585
- });
1586
- pipeline.push({
1587
- $match: {
1588
- clusterTime: { $gt: startAt },
1589
- $or: [
1590
- {
1591
- $expr: {
1592
- $ne: [
1593
- { $mergeObjects: ['$fullDocument', { touchedAt: null }] },
1594
- { $mergeObjects: ['$fullDocumentBeforeChange', { touchedAt: null }] },
1595
- ],
1596
- },
1597
- },
1598
- Object.fromEntries(changeKeys.map(k => [k, null])),
1599
- ],
1600
- },
1588
+ }
1589
+ async function* tailOplog(db, opts) {
1590
+ let lastTs = opts.since ?? (await getCurrentTimestamp(db));
1591
+ const reopenDelayMs = opts.reopenDelayMs ?? 250;
1592
+ const coll = db.client.db('local').collection('oplog.rs');
1593
+ while (true) {
1594
+ const cursor = coll.find({
1595
+ ts: { $gt: lastTs },
1596
+ ns: RegExp(`^${db.namespace}\\.(?!tmp_)(?!__).*(?<!_snapshot)$`),
1597
+ op: { $in: ['i', 'u'] },
1598
+ }, {
1599
+ tailable: true,
1600
+ awaitData: true,
1601
+ noCursorTimeout: true,
1602
+ });
1603
+ try {
1604
+ for await (const doc of cursor) {
1605
+ lastTs = doc.ts;
1606
+ if (doc.op === 'i') {
1607
+ yield { ns: doc.ns, fields: new Set(Object.keys(doc.o)), doc };
1608
+ }
1609
+ else {
1610
+ if (doc.o['$v'] !== 2) {
1611
+ throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc.o)}`);
1612
+ }
1613
+ const updatedFields = [];
1614
+ const diff = doc.o['diff'];
1615
+ for (const updateOp in diff) {
1616
+ if (['u', 'i', 'd'].includes(updateOp)) {
1617
+ updatedFields.push(...Object.keys(diff[updateOp]));
1618
+ }
1619
+ else if (updateOp.startsWith('s')) {
1620
+ updatedFields.push(updateOp.slice(1));
1621
+ }
1622
+ }
1623
+ yield { ns: doc.ns, fields: new Set(updatedFields), doc };
1624
+ }
1625
+ }
1626
+ }
1627
+ catch (e) {
1628
+ log('oplog loop error', e);
1629
+ }
1630
+ finally {
1631
+ log('oplog loop ended');
1632
+ await cursor.close().catch(() => { });
1633
+ }
1634
+ await sleep(reopenDelayMs);
1635
+ }
1636
+ }
1637
+ const watchers = new Map();
1638
+ let running = false;
1639
+ const loop = async (db) => {
1640
+ log('starting oplog loop');
1641
+ for await (const { ns, fields, doc } of tailOplog(db, {})) {
1642
+ const m = watchers.get(ns);
1643
+ if (!m)
1644
+ continue;
1645
+ for (const { cb, keys } of m.values()) {
1646
+ if (!keys || keys.some(k => fields.has(k))) {
1647
+ cb(doc);
1648
+ }
1649
+ }
1650
+ }
1651
+ };
1652
+ const register = (coll, keys, cb) => {
1653
+ const ns = coll.namespace;
1654
+ let m = watchers.get(ns);
1655
+ if (!m)
1656
+ watchers.set(ns, (m = new Map()));
1657
+ const id = crypto.randomUUID();
1658
+ m.set(id, { cb, keys });
1659
+ if (!running) {
1660
+ running = true;
1661
+ loop(coll.s.db);
1662
+ }
1663
+ return () => {
1664
+ m.delete(id);
1665
+ if (m.size === 0)
1666
+ watchers.delete(ns);
1667
+ };
1668
+ };
1669
+ const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
1670
+ const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
1671
+ let resolve = (_) => { };
1672
+ const promise = new Promise(r => (resolve = r));
1673
+ const close = register(collection, p ? Object.keys(projection) : null, (doc) => {
1674
+ log(streamName, 'change detected', doc);
1675
+ resolve(doc);
1676
+ close();
1601
1677
  });
1602
- pipeline.push({
1603
- $project: {
1604
- _id: 1,
1678
+ return {
1679
+ tryNext: async () => {
1680
+ const doc = await promise;
1681
+ const start = Date.now();
1682
+ await waitUntilStablePast(collection.s.db, doc.ts);
1683
+ log(streamName, 'stable past took', Date.now() - start);
1684
+ return doc;
1605
1685
  },
1606
- });
1607
- const stream = db.collection(collection.collectionName).watch(pipeline, {
1608
- fullDocument: 'required',
1609
- fullDocumentBeforeChange: 'required',
1610
- startAtOperationTime: startAt,
1611
- });
1612
- const tryNext = async () => {
1613
- const doc = await stream.tryNext();
1614
- if (doc)
1615
- await new Promise(resolve => setTimeout(resolve, 100));
1616
- if (doc)
1617
- log('detected', streamName, collection.collectionName, doc);
1618
- return doc;
1686
+ close: async () => close(),
1619
1687
  };
1620
- return { tryNext, close: () => stream.close() };
1621
1688
  };
1622
1689
 
1623
1690
  const actions = {
@@ -1650,10 +1717,54 @@ const getFirstStages = (view, needs) => {
1650
1717
  return { firstStages, hardMatch };
1651
1718
  };
1652
1719
 
1653
- const tryNext$1 = (stream) => stream.tryNext().catch(() => ({}));
1720
+ require('dotenv').config();
1721
+ const uri = process.env['MONGO_URL'];
1722
+
1723
+ const enablePreAndPostImages = (coll) => coll.s.db.command({
1724
+ collMod: coll.collectionName,
1725
+ changeStreamPreAndPostImages: { enabled: true },
1726
+ });
1727
+ const prepare = async (testName) => {
1728
+ const client = new mongodb.MongoClient(uri, testName ? { monitorCommands: true } : {});
1729
+ if (testName) {
1730
+ const handler = (c) => {
1731
+ promises.writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
1732
+ };
1733
+ client.on('commandStarted', handler);
1734
+ client.on('commandSucceeded', handler);
1735
+ }
1736
+ await client.connect();
1737
+ await client.db('admin').command({
1738
+ setClusterParameter: {
1739
+ changeStreamOptions: {
1740
+ preAndPostImages: { expireAfterSeconds: 60 },
1741
+ },
1742
+ },
1743
+ });
1744
+ return client;
1745
+ };
1746
+ const makeCol = async (docs, database, name) => {
1747
+ if (!name) {
1748
+ (name = crypto.randomUUID());
1749
+ }
1750
+ try {
1751
+ const col = await database.createCollection(name, {
1752
+ changeStreamPreAndPostImages: { enabled: true },
1753
+ });
1754
+ if (docs.length)
1755
+ await col.insertMany([...docs]);
1756
+ return col;
1757
+ }
1758
+ catch {
1759
+ return database.collection(name);
1760
+ }
1761
+ };
1762
+
1654
1763
  const streamNames = {};
1655
1764
  const executes$2 = (view, input, streamName, skip = false, after, needs = {}) => {
1656
1765
  const { collection, projection, match } = view;
1766
+ const client = prepare();
1767
+ const pdb = client.then(cl => cl.db(collection.dbName));
1657
1768
  const { firstStages, hardMatch } = getFirstStages(view, needs);
1658
1769
  const db = collection.s.db, coll = collection.collectionName;
1659
1770
  const hash = crypto$1
@@ -1676,10 +1787,22 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1676
1787
  : {}).catch(e => e.code == 86 || Promise.reject(e));
1677
1788
  const last = db.collection('__last');
1678
1789
  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
+ });
1679
1794
  createIndex(snapshotCollection, { updated: 1 }, {
1680
1795
  partialFilterExpression: { updated: true },
1681
1796
  name: 'updated_' + new mongodb.UUID().toString('base64'),
1682
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
+ });
1683
1806
  createIndex(snapshotCollection, { updated: 1 }, {
1684
1807
  partialFilterExpression: { updated: true, after: null, before: null },
1685
1808
  name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
@@ -1718,7 +1841,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1718
1841
  return next(step2, 'get last update');
1719
1842
  };
1720
1843
  const step2 = () => Promise.all([
1721
- last.findOne({ _id: streamName, data }),
1844
+ last.findOne({ _id: streamName, data, job: null }),
1722
1845
  last.findOne({ _id: streamName }),
1723
1846
  ]).then(ts => next(step2_5(ts), ts[0]
1724
1847
  ? `no teardown to handle, starting at ${ts[0].ts}`
@@ -1741,7 +1864,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1741
1864
  log('teardown done', `db['${snapshotCollection.collectionName}'].drop()`, ...out);
1742
1865
  };
1743
1866
  if (!same) {
1744
- log('not same, new data', data);
1867
+ log('not same, new data', streamName, data);
1745
1868
  await handleTeardown(exists ?? { data });
1746
1869
  }
1747
1870
  await after?.();
@@ -1770,40 +1893,46 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1770
1893
  whenMatched: link().with($replaceWith_(ite(eq(root().of('before').expr())(ctx()('new').of('after').expr()), root().expr(), mergeObjects(root().expr(), ctx()('new').expr())))).stages,
1771
1894
  whenNotMatched: 'insert',
1772
1895
  })).stages;
1773
- const r = await aggregate(streamName, c => c({ coll: collection, input: cloneIntoNew }));
1774
- await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
1896
+ const r = await aggregate(pdb, streamName, c => c({ coll: collection, input: cloneIntoNew }));
1897
+ const start = Date.now();
1898
+ const res = await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
1899
+ log('deleting from cloned into new collection', Date.now() - start, res, `db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null, before: null })`);
1775
1900
  return next(step4({ result: r, ts: lastTS?.ts }), 'run the aggregation');
1776
1901
  };
1777
- const makeStream = (startAt) => makeWatchStream(db, view, startAt, streamName);
1902
+ const makeStream = () => makeWatchStream(view, streamName);
1778
1903
  const step4 = ({ result, ts }) => async () => {
1779
1904
  const start = Date.now();
1780
- await snapshotCollection.updateMany({ before: null }, { $set: { before: null } });
1781
- const stages = finalInput.raw(ts === undefined);
1782
- const aggResult = await aggregate(streamName, c => c({
1905
+ log('snapshot', streamName, 'ensure before null', Date.now() - start);
1906
+ const first = ts === undefined;
1907
+ const stages = finalInput.raw(first);
1908
+ await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
1909
+ const stream = makeStream();
1910
+ const aggResult = await aggregate(pdb, streamName, c => c({
1783
1911
  coll: snapshotCollection,
1784
1912
  input: link()
1785
1913
  .with($match_(root().of('updated').has($eq(true))))
1914
+ .with($set_(set()({
1915
+ before: [
1916
+ 'before',
1917
+ to($ifNull(root().of('before').expr(), nil)),
1918
+ ],
1919
+ })))
1786
1920
  .with(input.delta)
1787
1921
  .with(stages).stages,
1788
1922
  }), false, start);
1789
- const stream = makeStream(result.cursor.atClusterTime);
1790
- const nextRes = tryNext$1(stream);
1791
- const intoColl = stages.at(-1).$merge.into.coll;
1792
- const startx = Date.now();
1793
- await db
1794
- .collection(intoColl)
1795
- .countDocuments({ touchedAt: { $gte: result.cursor.atClusterTime } })
1796
- .then(count => log(`documents updated ${intoColl}`, count, 'took', Date.now() - startx));
1797
- return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes }), 'remove handled deleted updated', () => stream.close());
1923
+ const nextRes = stream.tryNext();
1924
+ stages.at(-1).$merge.into.coll;
1925
+ return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
1798
1926
  };
1799
1927
  const step5 = (l) => async () => {
1800
- log(`remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
1928
+ log(streamName, `remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
1801
1929
  await snapshotCollection.deleteMany({ updated: true, after: null });
1802
1930
  log('removed handled deleted updated');
1803
1931
  return next(step6(l), 'update snapshot aggregation');
1804
1932
  };
1805
1933
  const step6 = (l) => async () => {
1806
1934
  log('update snapshot aggregation', `db['${snapshotCollection.collectionName}'].updateMany({ updated: true }, [ { $set: { updated: false, after: null, before: '$after' } } ])`);
1935
+ const start = Date.now();
1807
1936
  await snapshotCollection.updateMany({ updated: true }, [
1808
1937
  {
1809
1938
  $set: {
@@ -1813,23 +1942,27 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1813
1942
  },
1814
1943
  },
1815
1944
  ]);
1816
- log('updated snapshot aggregation');
1945
+ log('updated snapshot aggregation', Date.now() - start);
1817
1946
  return next(step7(l), 'update __last');
1818
1947
  };
1819
1948
  const step7 = (l) => async () => {
1820
- await last.updateOne({ _id: streamName }, {
1949
+ const start = Date.now();
1950
+ const patch = {
1821
1951
  $set: {
1822
1952
  ts: l.ts,
1823
- data,
1953
+ job: null,
1824
1954
  },
1825
- }, { upsert: true });
1955
+ };
1956
+ if (l.first)
1957
+ patch.$set = { ...patch.$set, data };
1958
+ await last.updateOne({ _id: streamName }, patch, { upsert: true });
1959
+ log('updated __last', Date.now() - start, `db['${last.collectionName}'].updateOne({ _id: '${streamName}' }, `, patch, `, { upsert: true })`);
1826
1960
  return step8(l);
1827
1961
  };
1828
1962
  const step8 = (l) => {
1829
- return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes
1830
- .then(doc => doc
1963
+ return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes.then(doc => doc
1831
1964
  ? next(step3({ _id: streamName, ts: l.ts }), 'restart')
1832
- : step8({ ...l, nextRes: tryNext$1(l.stream) })), 'wait for change');
1965
+ : step8({ ...l, nextRes: l.stream.tryNext() })), 'wait for change');
1833
1966
  };
1834
1967
  return skip
1835
1968
  ? withStop(() => synchronousPromise.SynchronousPromise.resolve(next(step3(null), 'clone into new collection')))
@@ -1847,7 +1980,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1847
1980
  };
1848
1981
  const staging = (view, streamName, skip = false, after) => pipe(input => executes$2(view, input, streamName, skip, after, view.needs), emptyDelta(), concatDelta, emptyDelta);
1849
1982
 
1850
- const tryNext = (stream) => stream.tryNext().catch(() => ({}));
1851
1983
  const executes$1 = (view, input, streamName, needs) => {
1852
1984
  const hash = crypto$1
1853
1985
  .createHash('md5')
@@ -1858,6 +1990,8 @@ const executes$1 = (view, input, streamName, needs) => {
1858
1990
  else if (streamNames[streamName] != hash)
1859
1991
  throw new Error('streamName already used');
1860
1992
  const { collection, projection, hardMatch: pre, match } = view;
1993
+ const client = prepare();
1994
+ const pdb = client.then(cl => cl.db(collection.dbName));
1861
1995
  const removeNotYetSynchronizedFields = projection &&
1862
1996
  Object.values(mapExactToObject(projection, (_, k) => (needs[k] ?? k.startsWith('_')) ? root().of(k).has($exists(true)) : null));
1863
1997
  const hardMatch = removeNotYetSynchronizedFields
@@ -1939,15 +2073,15 @@ const executes$1 = (view, input, streamName, needs) => {
1939
2073
  info: { debug: 'wait for clone into new collection', job: undefined },
1940
2074
  };
1941
2075
  };
1942
- const makeStream = (startAt) => makeWatchStream(db, view, startAt, streamName);
2076
+ const makeStream = () => makeWatchStream(view, streamName);
1943
2077
  const step4 = (lastTS) => async () => {
1944
2078
  const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
1945
- const aggResult = await aggregate(streamName, c => c({
2079
+ const stream = makeStream();
2080
+ const aggResult = await aggregate(pdb, streamName, c => c({
1946
2081
  coll: collection,
1947
2082
  input: raw,
1948
2083
  }));
1949
- const stream = makeStream(aggResult.cursor.atClusterTime);
1950
- const nextRes = tryNext(stream);
2084
+ const nextRes = stream.tryNext();
1951
2085
  return next(step7({ aggResult, ts: aggResult.cursor.atClusterTime, stream, nextRes }), 'update __last', () => stream.close());
1952
2086
  };
1953
2087
  const step7 = (l) => async () => {
@@ -1958,10 +2092,9 @@ const executes$1 = (view, input, streamName, needs) => {
1958
2092
  return {
1959
2093
  data: l.aggResult.cursor.firstBatch,
1960
2094
  info: { job: undefined, debug: 'wait for change' },
1961
- cont: withStop(() => l.nextRes
1962
- .then(doc => doc
2095
+ cont: withStop(() => l.nextRes.then(doc => doc
1963
2096
  ? next(step4({ _id: streamName, ts: l.ts }), 'restart')
1964
- : step8({ ...l, nextRes: tryNext(l.stream) }))),
2097
+ : step8({ ...l, nextRes: l.stream.tryNext() }))),
1965
2098
  };
1966
2099
  };
1967
2100
  return stop;
@@ -1988,49 +2121,6 @@ const executes = (view, input, needs) => {
1988
2121
  };
1989
2122
  const single = (view, needs = {}) => pipe(input => executes(view, input, needs), emptyDelta(), concatDelta, emptyDelta);
1990
2123
 
1991
- require('dotenv').config();
1992
- const uri = process.env['MONGO_URL'];
1993
-
1994
- const enablePreAndPostImages = (coll) => coll.s.db.command({
1995
- collMod: coll.collectionName,
1996
- changeStreamPreAndPostImages: { enabled: true },
1997
- });
1998
- const prepare = async (testName) => {
1999
- const client = new mongodb.MongoClient(uri, testName ? { monitorCommands: true } : {});
2000
- if (testName) {
2001
- const handler = (c) => {
2002
- promises.writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
2003
- };
2004
- client.on('commandStarted', handler);
2005
- client.on('commandSucceeded', handler);
2006
- }
2007
- await client.connect();
2008
- await client.db('admin').command({
2009
- setClusterParameter: {
2010
- changeStreamOptions: {
2011
- preAndPostImages: { expireAfterSeconds: 60 },
2012
- },
2013
- },
2014
- });
2015
- return client;
2016
- };
2017
- const makeCol = async (docs, database, name) => {
2018
- if (!name) {
2019
- (name = crypto.randomUUID());
2020
- }
2021
- try {
2022
- const col = await database.createCollection(name, {
2023
- changeStreamPreAndPostImages: { enabled: true },
2024
- });
2025
- if (docs.length)
2026
- await col.insertMany([...docs]);
2027
- return col;
2028
- }
2029
- catch {
2030
- return database.collection(name);
2031
- }
2032
- };
2033
-
2034
2124
  exports.$accumulator = $accumulator;
2035
2125
  exports.$and = $and;
2036
2126
  exports.$countDict = $countDict;
@@ -2155,6 +2245,7 @@ exports.range = range;
2155
2245
  exports.regex = regex;
2156
2246
  exports.root = root;
2157
2247
  exports.set = set;
2248
+ exports.setF = setF;
2158
2249
  exports.setField = setField;
2159
2250
  exports.single = single;
2160
2251
  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.19",
6
+ "version": "0.1.21",
7
7
  "dependencies": {
8
8
  "dayjs": "^1.11.9",
9
9
  "dotenv": "^16.3.1",