@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.
- package/index.d.ts +5 -1
- package/index.esm.js +230 -140
- package/index.js +229 -138
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
1900
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1776
1901
|
const step4 = ({ result, ts }) => async () => {
|
|
1777
1902
|
const start = Date.now();
|
|
1778
|
-
|
|
1779
|
-
const
|
|
1780
|
-
const
|
|
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
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1947
|
+
const start = Date.now();
|
|
1948
|
+
const patch = {
|
|
1819
1949
|
$set: {
|
|
1820
1950
|
ts: l.ts,
|
|
1821
|
-
|
|
1951
|
+
job: null,
|
|
1822
1952
|
},
|
|
1823
|
-
}
|
|
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:
|
|
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 = (
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1561
|
-
const
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
1902
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1778
1903
|
const step4 = ({ result, ts }) => async () => {
|
|
1779
1904
|
const start = Date.now();
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
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
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
1949
|
+
const start = Date.now();
|
|
1950
|
+
const patch = {
|
|
1821
1951
|
$set: {
|
|
1822
1952
|
ts: l.ts,
|
|
1823
|
-
|
|
1953
|
+
job: null,
|
|
1824
1954
|
},
|
|
1825
|
-
}
|
|
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:
|
|
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 = (
|
|
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
|
|
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
|
|
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:
|
|
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;
|