@omegup/msync 0.1.19 → 0.1.20
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 -139
- package/index.js +229 -137
- 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,129 @@ 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
|
+
log('oplog event', ns, doc.op, [...fields]);
|
|
1641
|
+
const m = watchers.get(ns);
|
|
1642
|
+
if (!m)
|
|
1643
|
+
continue;
|
|
1644
|
+
for (const { cb, keys } of m.values()) {
|
|
1645
|
+
if (!keys || keys.some(k => fields.has(k))) {
|
|
1646
|
+
cb(doc);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
const register = (coll, keys, cb) => {
|
|
1652
|
+
const ns = coll.namespace;
|
|
1653
|
+
let m = watchers.get(ns);
|
|
1654
|
+
if (!m)
|
|
1655
|
+
watchers.set(ns, (m = new Map()));
|
|
1656
|
+
const id = crypto.randomUUID();
|
|
1657
|
+
m.set(id, { cb, keys });
|
|
1658
|
+
if (!running) {
|
|
1659
|
+
running = true;
|
|
1660
|
+
loop(coll.s.db);
|
|
1661
|
+
}
|
|
1662
|
+
return () => {
|
|
1663
|
+
m.delete(id);
|
|
1664
|
+
if (m.size === 0)
|
|
1665
|
+
watchers.delete(ns);
|
|
1666
|
+
};
|
|
1667
|
+
};
|
|
1668
|
+
const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
|
|
1669
|
+
const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
|
|
1670
|
+
let resolve = (_) => { };
|
|
1671
|
+
const promise = new Promise(r => (resolve = r));
|
|
1672
|
+
const close = register(collection, p ? Object.keys(projection) : null, (doc) => {
|
|
1673
|
+
log(streamName, 'change detected', doc);
|
|
1674
|
+
resolve(doc);
|
|
1675
|
+
close();
|
|
1599
1676
|
});
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1677
|
+
return {
|
|
1678
|
+
tryNext: async () => {
|
|
1679
|
+
const doc = await promise;
|
|
1680
|
+
const start = Date.now();
|
|
1681
|
+
await waitUntilStablePast(collection.s.db, doc.ts);
|
|
1682
|
+
log(streamName, 'stable past took', Date.now() - start);
|
|
1683
|
+
return doc;
|
|
1603
1684
|
},
|
|
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;
|
|
1685
|
+
close: async () => close(),
|
|
1617
1686
|
};
|
|
1618
|
-
return { tryNext, close: () => stream.close() };
|
|
1619
1687
|
};
|
|
1620
1688
|
|
|
1621
1689
|
const actions = {
|
|
@@ -1648,10 +1716,54 @@ const getFirstStages = (view, needs) => {
|
|
|
1648
1716
|
return { firstStages, hardMatch };
|
|
1649
1717
|
};
|
|
1650
1718
|
|
|
1651
|
-
|
|
1719
|
+
require('dotenv').config();
|
|
1720
|
+
const uri = process.env['MONGO_URL'];
|
|
1721
|
+
|
|
1722
|
+
const enablePreAndPostImages = (coll) => coll.s.db.command({
|
|
1723
|
+
collMod: coll.collectionName,
|
|
1724
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1725
|
+
});
|
|
1726
|
+
const prepare = async (testName) => {
|
|
1727
|
+
const client = new MongoClient(uri, testName ? { monitorCommands: true } : {});
|
|
1728
|
+
if (testName) {
|
|
1729
|
+
const handler = (c) => {
|
|
1730
|
+
writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
|
|
1731
|
+
};
|
|
1732
|
+
client.on('commandStarted', handler);
|
|
1733
|
+
client.on('commandSucceeded', handler);
|
|
1734
|
+
}
|
|
1735
|
+
await client.connect();
|
|
1736
|
+
await client.db('admin').command({
|
|
1737
|
+
setClusterParameter: {
|
|
1738
|
+
changeStreamOptions: {
|
|
1739
|
+
preAndPostImages: { expireAfterSeconds: 60 },
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
});
|
|
1743
|
+
return client;
|
|
1744
|
+
};
|
|
1745
|
+
const makeCol = async (docs, database, name) => {
|
|
1746
|
+
if (!name) {
|
|
1747
|
+
(name = crypto.randomUUID());
|
|
1748
|
+
}
|
|
1749
|
+
try {
|
|
1750
|
+
const col = await database.createCollection(name, {
|
|
1751
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1752
|
+
});
|
|
1753
|
+
if (docs.length)
|
|
1754
|
+
await col.insertMany([...docs]);
|
|
1755
|
+
return col;
|
|
1756
|
+
}
|
|
1757
|
+
catch {
|
|
1758
|
+
return database.collection(name);
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1652
1762
|
const streamNames = {};
|
|
1653
1763
|
const executes$2 = (view, input, streamName, skip = false, after, needs = {}) => {
|
|
1654
1764
|
const { collection, projection, match } = view;
|
|
1765
|
+
const client = prepare();
|
|
1766
|
+
const pdb = client.then(cl => cl.db(collection.dbName));
|
|
1655
1767
|
const { firstStages, hardMatch } = getFirstStages(view, needs);
|
|
1656
1768
|
const db = collection.s.db, coll = collection.collectionName;
|
|
1657
1769
|
const hash = crypto$1
|
|
@@ -1674,10 +1786,22 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1674
1786
|
: {}).catch(e => e.code == 86 || Promise.reject(e));
|
|
1675
1787
|
const last = db.collection('__last');
|
|
1676
1788
|
const snapshotCollection = db.collection(coll + '_' + streamName + '_snapshot');
|
|
1789
|
+
createIndex(snapshotCollection, { before: 1 }, {
|
|
1790
|
+
partialFilterExpression: { before: null },
|
|
1791
|
+
name: 'before_' + new UUID().toString('base64'),
|
|
1792
|
+
});
|
|
1677
1793
|
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1678
1794
|
partialFilterExpression: { updated: true },
|
|
1679
1795
|
name: 'updated_' + new UUID().toString('base64'),
|
|
1680
1796
|
});
|
|
1797
|
+
createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1798
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1799
|
+
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
1800
|
+
});
|
|
1801
|
+
createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1802
|
+
partialFilterExpression: { updated: true, after: null },
|
|
1803
|
+
name: 'updated_no_after_' + new UUID().toString('base64'),
|
|
1804
|
+
});
|
|
1681
1805
|
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1682
1806
|
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1683
1807
|
name: 'updated_nulls_' + new UUID().toString('base64'),
|
|
@@ -1716,7 +1840,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1716
1840
|
return next(step2, 'get last update');
|
|
1717
1841
|
};
|
|
1718
1842
|
const step2 = () => Promise.all([
|
|
1719
|
-
last.findOne({ _id: streamName, data }),
|
|
1843
|
+
last.findOne({ _id: streamName, data, job: null }),
|
|
1720
1844
|
last.findOne({ _id: streamName }),
|
|
1721
1845
|
]).then(ts => next(step2_5(ts), ts[0]
|
|
1722
1846
|
? `no teardown to handle, starting at ${ts[0].ts}`
|
|
@@ -1768,40 +1892,46 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1768
1892
|
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
1893
|
whenNotMatched: 'insert',
|
|
1770
1894
|
})).stages;
|
|
1771
|
-
const r = await aggregate(streamName, c => c({ coll: collection, input: cloneIntoNew }));
|
|
1772
|
-
|
|
1895
|
+
const r = await aggregate(pdb, streamName, c => c({ coll: collection, input: cloneIntoNew }));
|
|
1896
|
+
const start = Date.now();
|
|
1897
|
+
const res = await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
|
|
1898
|
+
log('deleting from cloned into new collection', Date.now() - start, res, `db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null, before: null })`);
|
|
1773
1899
|
return next(step4({ result: r, ts: lastTS?.ts }), 'run the aggregation');
|
|
1774
1900
|
};
|
|
1775
|
-
const makeStream = (
|
|
1901
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1776
1902
|
const step4 = ({ result, ts }) => async () => {
|
|
1777
1903
|
const start = Date.now();
|
|
1778
|
-
|
|
1779
|
-
const
|
|
1780
|
-
const
|
|
1904
|
+
log('snapshot', streamName, 'ensure before null', Date.now() - start);
|
|
1905
|
+
const first = ts === undefined;
|
|
1906
|
+
const stages = finalInput.raw(first);
|
|
1907
|
+
await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
|
|
1908
|
+
const stream = makeStream();
|
|
1909
|
+
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1781
1910
|
coll: snapshotCollection,
|
|
1782
1911
|
input: link()
|
|
1783
1912
|
.with($match_(root().of('updated').has($eq(true))))
|
|
1913
|
+
.with($set_(set()({
|
|
1914
|
+
before: [
|
|
1915
|
+
'before',
|
|
1916
|
+
to($ifNull(root().of('before').expr(), nil)),
|
|
1917
|
+
],
|
|
1918
|
+
})))
|
|
1784
1919
|
.with(input.delta)
|
|
1785
1920
|
.with(stages).stages,
|
|
1786
1921
|
}), 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());
|
|
1922
|
+
const nextRes = stream.tryNext();
|
|
1923
|
+
stages.at(-1).$merge.into.coll;
|
|
1924
|
+
return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
|
|
1796
1925
|
};
|
|
1797
1926
|
const step5 = (l) => async () => {
|
|
1798
|
-
log(`remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
|
|
1927
|
+
log(streamName, `remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
|
|
1799
1928
|
await snapshotCollection.deleteMany({ updated: true, after: null });
|
|
1800
1929
|
log('removed handled deleted updated');
|
|
1801
1930
|
return next(step6(l), 'update snapshot aggregation');
|
|
1802
1931
|
};
|
|
1803
1932
|
const step6 = (l) => async () => {
|
|
1804
1933
|
log('update snapshot aggregation', `db['${snapshotCollection.collectionName}'].updateMany({ updated: true }, [ { $set: { updated: false, after: null, before: '$after' } } ])`);
|
|
1934
|
+
const start = Date.now();
|
|
1805
1935
|
await snapshotCollection.updateMany({ updated: true }, [
|
|
1806
1936
|
{
|
|
1807
1937
|
$set: {
|
|
@@ -1811,23 +1941,27 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1811
1941
|
},
|
|
1812
1942
|
},
|
|
1813
1943
|
]);
|
|
1814
|
-
log('updated snapshot aggregation');
|
|
1944
|
+
log('updated snapshot aggregation', Date.now() - start);
|
|
1815
1945
|
return next(step7(l), 'update __last');
|
|
1816
1946
|
};
|
|
1817
1947
|
const step7 = (l) => async () => {
|
|
1818
|
-
|
|
1948
|
+
const start = Date.now();
|
|
1949
|
+
const patch = {
|
|
1819
1950
|
$set: {
|
|
1820
1951
|
ts: l.ts,
|
|
1821
|
-
|
|
1952
|
+
job: null,
|
|
1822
1953
|
},
|
|
1823
|
-
}
|
|
1954
|
+
};
|
|
1955
|
+
if (l.ts)
|
|
1956
|
+
patch.$set = data;
|
|
1957
|
+
await last.updateOne({ _id: streamName }, patch, { upsert: true });
|
|
1958
|
+
log('updated __last', Date.now() - start, `db['${last.collectionName}'].updateOne({ _id: '${streamName}' }, `, patch, `, { upsert: true })`);
|
|
1824
1959
|
return step8(l);
|
|
1825
1960
|
};
|
|
1826
1961
|
const step8 = (l) => {
|
|
1827
|
-
return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes
|
|
1828
|
-
.then(doc => doc
|
|
1962
|
+
return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes.then(doc => doc
|
|
1829
1963
|
? next(step3({ _id: streamName, ts: l.ts }), 'restart')
|
|
1830
|
-
: step8({ ...l, nextRes:
|
|
1964
|
+
: step8({ ...l, nextRes: l.stream.tryNext() })), 'wait for change');
|
|
1831
1965
|
};
|
|
1832
1966
|
return skip
|
|
1833
1967
|
? withStop(() => SynchronousPromise.resolve(next(step3(null), 'clone into new collection')))
|
|
@@ -1845,7 +1979,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1845
1979
|
};
|
|
1846
1980
|
const staging = (view, streamName, skip = false, after) => pipe(input => executes$2(view, input, streamName, skip, after, view.needs), emptyDelta(), concatDelta, emptyDelta);
|
|
1847
1981
|
|
|
1848
|
-
const tryNext = (stream) => stream.tryNext().catch(() => ({}));
|
|
1849
1982
|
const executes$1 = (view, input, streamName, needs) => {
|
|
1850
1983
|
const hash = crypto$1
|
|
1851
1984
|
.createHash('md5')
|
|
@@ -1856,6 +1989,8 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1856
1989
|
else if (streamNames[streamName] != hash)
|
|
1857
1990
|
throw new Error('streamName already used');
|
|
1858
1991
|
const { collection, projection, hardMatch: pre, match } = view;
|
|
1992
|
+
const client = prepare();
|
|
1993
|
+
const pdb = client.then(cl => cl.db(collection.dbName));
|
|
1859
1994
|
const removeNotYetSynchronizedFields = projection &&
|
|
1860
1995
|
Object.values(mapExactToObject(projection, (_, k) => (needs[k] ?? k.startsWith('_')) ? root().of(k).has($exists(true)) : null));
|
|
1861
1996
|
const hardMatch = removeNotYetSynchronizedFields
|
|
@@ -1937,15 +2072,15 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1937
2072
|
info: { debug: 'wait for clone into new collection', job: undefined },
|
|
1938
2073
|
};
|
|
1939
2074
|
};
|
|
1940
|
-
const makeStream = (
|
|
2075
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1941
2076
|
const step4 = (lastTS) => async () => {
|
|
1942
2077
|
const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
|
|
1943
|
-
const
|
|
2078
|
+
const stream = makeStream();
|
|
2079
|
+
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1944
2080
|
coll: collection,
|
|
1945
2081
|
input: raw,
|
|
1946
2082
|
}));
|
|
1947
|
-
const
|
|
1948
|
-
const nextRes = tryNext(stream);
|
|
2083
|
+
const nextRes = stream.tryNext();
|
|
1949
2084
|
return next(step7({ aggResult, ts: aggResult.cursor.atClusterTime, stream, nextRes }), 'update __last', () => stream.close());
|
|
1950
2085
|
};
|
|
1951
2086
|
const step7 = (l) => async () => {
|
|
@@ -1956,10 +2091,9 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1956
2091
|
return {
|
|
1957
2092
|
data: l.aggResult.cursor.firstBatch,
|
|
1958
2093
|
info: { job: undefined, debug: 'wait for change' },
|
|
1959
|
-
cont: withStop(() => l.nextRes
|
|
1960
|
-
.then(doc => doc
|
|
2094
|
+
cont: withStop(() => l.nextRes.then(doc => doc
|
|
1961
2095
|
? next(step4({ _id: streamName, ts: l.ts }), 'restart')
|
|
1962
|
-
: step8({ ...l, nextRes:
|
|
2096
|
+
: step8({ ...l, nextRes: l.stream.tryNext() }))),
|
|
1963
2097
|
};
|
|
1964
2098
|
};
|
|
1965
2099
|
return stop;
|
|
@@ -1986,47 +2120,4 @@ const executes = (view, input, needs) => {
|
|
|
1986
2120
|
};
|
|
1987
2121
|
const single = (view, needs = {}) => pipe(input => executes(view, input, needs), emptyDelta(), concatDelta, emptyDelta);
|
|
1988
2122
|
|
|
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 };
|
|
2123
|
+
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,129 @@ 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
|
+
log('oplog event', ns, doc.op, [...fields]);
|
|
1643
|
+
const m = watchers.get(ns);
|
|
1644
|
+
if (!m)
|
|
1645
|
+
continue;
|
|
1646
|
+
for (const { cb, keys } of m.values()) {
|
|
1647
|
+
if (!keys || keys.some(k => fields.has(k))) {
|
|
1648
|
+
cb(doc);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
const register = (coll, keys, cb) => {
|
|
1654
|
+
const ns = coll.namespace;
|
|
1655
|
+
let m = watchers.get(ns);
|
|
1656
|
+
if (!m)
|
|
1657
|
+
watchers.set(ns, (m = new Map()));
|
|
1658
|
+
const id = crypto.randomUUID();
|
|
1659
|
+
m.set(id, { cb, keys });
|
|
1660
|
+
if (!running) {
|
|
1661
|
+
running = true;
|
|
1662
|
+
loop(coll.s.db);
|
|
1663
|
+
}
|
|
1664
|
+
return () => {
|
|
1665
|
+
m.delete(id);
|
|
1666
|
+
if (m.size === 0)
|
|
1667
|
+
watchers.delete(ns);
|
|
1668
|
+
};
|
|
1669
|
+
};
|
|
1670
|
+
const makeWatchStream = ({ collection, projection: p, hardMatch: m }, streamName) => {
|
|
1671
|
+
const projection = { ...(p ? mapExactToObject(p, v => v) : {}), deletedAt: 1 };
|
|
1672
|
+
let resolve = (_) => { };
|
|
1673
|
+
const promise = new Promise(r => (resolve = r));
|
|
1674
|
+
const close = register(collection, p ? Object.keys(projection) : null, (doc) => {
|
|
1675
|
+
log(streamName, 'change detected', doc);
|
|
1676
|
+
resolve(doc);
|
|
1677
|
+
close();
|
|
1601
1678
|
});
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1679
|
+
return {
|
|
1680
|
+
tryNext: async () => {
|
|
1681
|
+
const doc = await promise;
|
|
1682
|
+
const start = Date.now();
|
|
1683
|
+
await waitUntilStablePast(collection.s.db, doc.ts);
|
|
1684
|
+
log(streamName, 'stable past took', Date.now() - start);
|
|
1685
|
+
return doc;
|
|
1605
1686
|
},
|
|
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;
|
|
1687
|
+
close: async () => close(),
|
|
1619
1688
|
};
|
|
1620
|
-
return { tryNext, close: () => stream.close() };
|
|
1621
1689
|
};
|
|
1622
1690
|
|
|
1623
1691
|
const actions = {
|
|
@@ -1650,10 +1718,54 @@ const getFirstStages = (view, needs) => {
|
|
|
1650
1718
|
return { firstStages, hardMatch };
|
|
1651
1719
|
};
|
|
1652
1720
|
|
|
1653
|
-
|
|
1721
|
+
require('dotenv').config();
|
|
1722
|
+
const uri = process.env['MONGO_URL'];
|
|
1723
|
+
|
|
1724
|
+
const enablePreAndPostImages = (coll) => coll.s.db.command({
|
|
1725
|
+
collMod: coll.collectionName,
|
|
1726
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1727
|
+
});
|
|
1728
|
+
const prepare = async (testName) => {
|
|
1729
|
+
const client = new mongodb.MongoClient(uri, testName ? { monitorCommands: true } : {});
|
|
1730
|
+
if (testName) {
|
|
1731
|
+
const handler = (c) => {
|
|
1732
|
+
promises.writeFile(`./out/${testName}.log`, JSON.stringify(c.command) + ',\n', { flag: 'w' });
|
|
1733
|
+
};
|
|
1734
|
+
client.on('commandStarted', handler);
|
|
1735
|
+
client.on('commandSucceeded', handler);
|
|
1736
|
+
}
|
|
1737
|
+
await client.connect();
|
|
1738
|
+
await client.db('admin').command({
|
|
1739
|
+
setClusterParameter: {
|
|
1740
|
+
changeStreamOptions: {
|
|
1741
|
+
preAndPostImages: { expireAfterSeconds: 60 },
|
|
1742
|
+
},
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
return client;
|
|
1746
|
+
};
|
|
1747
|
+
const makeCol = async (docs, database, name) => {
|
|
1748
|
+
if (!name) {
|
|
1749
|
+
(name = crypto.randomUUID());
|
|
1750
|
+
}
|
|
1751
|
+
try {
|
|
1752
|
+
const col = await database.createCollection(name, {
|
|
1753
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
1754
|
+
});
|
|
1755
|
+
if (docs.length)
|
|
1756
|
+
await col.insertMany([...docs]);
|
|
1757
|
+
return col;
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
return database.collection(name);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1654
1764
|
const streamNames = {};
|
|
1655
1765
|
const executes$2 = (view, input, streamName, skip = false, after, needs = {}) => {
|
|
1656
1766
|
const { collection, projection, match } = view;
|
|
1767
|
+
const client = prepare();
|
|
1768
|
+
const pdb = client.then(cl => cl.db(collection.dbName));
|
|
1657
1769
|
const { firstStages, hardMatch } = getFirstStages(view, needs);
|
|
1658
1770
|
const db = collection.s.db, coll = collection.collectionName;
|
|
1659
1771
|
const hash = crypto$1
|
|
@@ -1676,10 +1788,22 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1676
1788
|
: {}).catch(e => e.code == 86 || Promise.reject(e));
|
|
1677
1789
|
const last = db.collection('__last');
|
|
1678
1790
|
const snapshotCollection = db.collection(coll + '_' + streamName + '_snapshot');
|
|
1791
|
+
createIndex(snapshotCollection, { before: 1 }, {
|
|
1792
|
+
partialFilterExpression: { before: null },
|
|
1793
|
+
name: 'before_' + new mongodb.UUID().toString('base64'),
|
|
1794
|
+
});
|
|
1679
1795
|
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1680
1796
|
partialFilterExpression: { updated: true },
|
|
1681
1797
|
name: 'updated_' + new mongodb.UUID().toString('base64'),
|
|
1682
1798
|
});
|
|
1799
|
+
createIndex(snapshotCollection, { updated: 1, after: 1, before: 1 }, {
|
|
1800
|
+
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1801
|
+
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
1802
|
+
});
|
|
1803
|
+
createIndex(snapshotCollection, { updated: 1, after: 1 }, {
|
|
1804
|
+
partialFilterExpression: { updated: true, after: null },
|
|
1805
|
+
name: 'updated_no_after_' + new mongodb.UUID().toString('base64'),
|
|
1806
|
+
});
|
|
1683
1807
|
createIndex(snapshotCollection, { updated: 1 }, {
|
|
1684
1808
|
partialFilterExpression: { updated: true, after: null, before: null },
|
|
1685
1809
|
name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
|
|
@@ -1718,7 +1842,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1718
1842
|
return next(step2, 'get last update');
|
|
1719
1843
|
};
|
|
1720
1844
|
const step2 = () => Promise.all([
|
|
1721
|
-
last.findOne({ _id: streamName, data }),
|
|
1845
|
+
last.findOne({ _id: streamName, data, job: null }),
|
|
1722
1846
|
last.findOne({ _id: streamName }),
|
|
1723
1847
|
]).then(ts => next(step2_5(ts), ts[0]
|
|
1724
1848
|
? `no teardown to handle, starting at ${ts[0].ts}`
|
|
@@ -1770,40 +1894,46 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1770
1894
|
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
1895
|
whenNotMatched: 'insert',
|
|
1772
1896
|
})).stages;
|
|
1773
|
-
const r = await aggregate(streamName, c => c({ coll: collection, input: cloneIntoNew }));
|
|
1774
|
-
|
|
1897
|
+
const r = await aggregate(pdb, streamName, c => c({ coll: collection, input: cloneIntoNew }));
|
|
1898
|
+
const start = Date.now();
|
|
1899
|
+
const res = await snapshotCollection.deleteMany({ updated: true, after: null, before: null });
|
|
1900
|
+
log('deleting from cloned into new collection', Date.now() - start, res, `db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null, before: null })`);
|
|
1775
1901
|
return next(step4({ result: r, ts: lastTS?.ts }), 'run the aggregation');
|
|
1776
1902
|
};
|
|
1777
|
-
const makeStream = (
|
|
1903
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1778
1904
|
const step4 = ({ result, ts }) => async () => {
|
|
1779
1905
|
const start = Date.now();
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
1906
|
+
log('snapshot', streamName, 'ensure before null', Date.now() - start);
|
|
1907
|
+
const first = ts === undefined;
|
|
1908
|
+
const stages = finalInput.raw(first);
|
|
1909
|
+
await last.updateOne({ _id: streamName }, { $set: { job: 1 } }, { upsert: true });
|
|
1910
|
+
const stream = makeStream();
|
|
1911
|
+
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1783
1912
|
coll: snapshotCollection,
|
|
1784
1913
|
input: link()
|
|
1785
1914
|
.with($match_(root().of('updated').has($eq(true))))
|
|
1915
|
+
.with($set_(set()({
|
|
1916
|
+
before: [
|
|
1917
|
+
'before',
|
|
1918
|
+
to($ifNull(root().of('before').expr(), nil)),
|
|
1919
|
+
],
|
|
1920
|
+
})))
|
|
1786
1921
|
.with(input.delta)
|
|
1787
1922
|
.with(stages).stages,
|
|
1788
1923
|
}), 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());
|
|
1924
|
+
const nextRes = stream.tryNext();
|
|
1925
|
+
stages.at(-1).$merge.into.coll;
|
|
1926
|
+
return next(step5({ ts: result.cursor.atClusterTime, aggResult, stream, nextRes, first }), 'remove handled deleted updated', () => stream.close());
|
|
1798
1927
|
};
|
|
1799
1928
|
const step5 = (l) => async () => {
|
|
1800
|
-
log(`remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
|
|
1929
|
+
log(streamName, `remove handled deleted updated db['${snapshotCollection.collectionName}'].deleteMany({ updated: true, after: null })`);
|
|
1801
1930
|
await snapshotCollection.deleteMany({ updated: true, after: null });
|
|
1802
1931
|
log('removed handled deleted updated');
|
|
1803
1932
|
return next(step6(l), 'update snapshot aggregation');
|
|
1804
1933
|
};
|
|
1805
1934
|
const step6 = (l) => async () => {
|
|
1806
1935
|
log('update snapshot aggregation', `db['${snapshotCollection.collectionName}'].updateMany({ updated: true }, [ { $set: { updated: false, after: null, before: '$after' } } ])`);
|
|
1936
|
+
const start = Date.now();
|
|
1807
1937
|
await snapshotCollection.updateMany({ updated: true }, [
|
|
1808
1938
|
{
|
|
1809
1939
|
$set: {
|
|
@@ -1813,23 +1943,27 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1813
1943
|
},
|
|
1814
1944
|
},
|
|
1815
1945
|
]);
|
|
1816
|
-
log('updated snapshot aggregation');
|
|
1946
|
+
log('updated snapshot aggregation', Date.now() - start);
|
|
1817
1947
|
return next(step7(l), 'update __last');
|
|
1818
1948
|
};
|
|
1819
1949
|
const step7 = (l) => async () => {
|
|
1820
|
-
|
|
1950
|
+
const start = Date.now();
|
|
1951
|
+
const patch = {
|
|
1821
1952
|
$set: {
|
|
1822
1953
|
ts: l.ts,
|
|
1823
|
-
|
|
1954
|
+
job: null,
|
|
1824
1955
|
},
|
|
1825
|
-
}
|
|
1956
|
+
};
|
|
1957
|
+
if (l.ts)
|
|
1958
|
+
patch.$set = data;
|
|
1959
|
+
await last.updateOne({ _id: streamName }, patch, { upsert: true });
|
|
1960
|
+
log('updated __last', Date.now() - start, `db['${last.collectionName}'].updateOne({ _id: '${streamName}' }, `, patch, `, { upsert: true })`);
|
|
1826
1961
|
return step8(l);
|
|
1827
1962
|
};
|
|
1828
1963
|
const step8 = (l) => {
|
|
1829
|
-
return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes
|
|
1830
|
-
.then(doc => doc
|
|
1964
|
+
return nextData(l.aggResult.cursor.firstBatch)(() => l.nextRes.then(doc => doc
|
|
1831
1965
|
? next(step3({ _id: streamName, ts: l.ts }), 'restart')
|
|
1832
|
-
: step8({ ...l, nextRes:
|
|
1966
|
+
: step8({ ...l, nextRes: l.stream.tryNext() })), 'wait for change');
|
|
1833
1967
|
};
|
|
1834
1968
|
return skip
|
|
1835
1969
|
? withStop(() => synchronousPromise.SynchronousPromise.resolve(next(step3(null), 'clone into new collection')))
|
|
@@ -1847,7 +1981,6 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
|
|
|
1847
1981
|
};
|
|
1848
1982
|
const staging = (view, streamName, skip = false, after) => pipe(input => executes$2(view, input, streamName, skip, after, view.needs), emptyDelta(), concatDelta, emptyDelta);
|
|
1849
1983
|
|
|
1850
|
-
const tryNext = (stream) => stream.tryNext().catch(() => ({}));
|
|
1851
1984
|
const executes$1 = (view, input, streamName, needs) => {
|
|
1852
1985
|
const hash = crypto$1
|
|
1853
1986
|
.createHash('md5')
|
|
@@ -1858,6 +1991,8 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1858
1991
|
else if (streamNames[streamName] != hash)
|
|
1859
1992
|
throw new Error('streamName already used');
|
|
1860
1993
|
const { collection, projection, hardMatch: pre, match } = view;
|
|
1994
|
+
const client = prepare();
|
|
1995
|
+
const pdb = client.then(cl => cl.db(collection.dbName));
|
|
1861
1996
|
const removeNotYetSynchronizedFields = projection &&
|
|
1862
1997
|
Object.values(mapExactToObject(projection, (_, k) => (needs[k] ?? k.startsWith('_')) ? root().of(k).has($exists(true)) : null));
|
|
1863
1998
|
const hardMatch = removeNotYetSynchronizedFields
|
|
@@ -1939,15 +2074,15 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1939
2074
|
info: { debug: 'wait for clone into new collection', job: undefined },
|
|
1940
2075
|
};
|
|
1941
2076
|
};
|
|
1942
|
-
const makeStream = (
|
|
2077
|
+
const makeStream = () => makeWatchStream(view, streamName);
|
|
1943
2078
|
const step4 = (lastTS) => async () => {
|
|
1944
2079
|
const raw = stages(lastTS).with(finalInput.raw(lastTS === null)).stages;
|
|
1945
|
-
const
|
|
2080
|
+
const stream = makeStream();
|
|
2081
|
+
const aggResult = await aggregate(pdb, streamName, c => c({
|
|
1946
2082
|
coll: collection,
|
|
1947
2083
|
input: raw,
|
|
1948
2084
|
}));
|
|
1949
|
-
const
|
|
1950
|
-
const nextRes = tryNext(stream);
|
|
2085
|
+
const nextRes = stream.tryNext();
|
|
1951
2086
|
return next(step7({ aggResult, ts: aggResult.cursor.atClusterTime, stream, nextRes }), 'update __last', () => stream.close());
|
|
1952
2087
|
};
|
|
1953
2088
|
const step7 = (l) => async () => {
|
|
@@ -1958,10 +2093,9 @@ const executes$1 = (view, input, streamName, needs) => {
|
|
|
1958
2093
|
return {
|
|
1959
2094
|
data: l.aggResult.cursor.firstBatch,
|
|
1960
2095
|
info: { job: undefined, debug: 'wait for change' },
|
|
1961
|
-
cont: withStop(() => l.nextRes
|
|
1962
|
-
.then(doc => doc
|
|
2096
|
+
cont: withStop(() => l.nextRes.then(doc => doc
|
|
1963
2097
|
? next(step4({ _id: streamName, ts: l.ts }), 'restart')
|
|
1964
|
-
: step8({ ...l, nextRes:
|
|
2098
|
+
: step8({ ...l, nextRes: l.stream.tryNext() }))),
|
|
1965
2099
|
};
|
|
1966
2100
|
};
|
|
1967
2101
|
return stop;
|
|
@@ -1988,49 +2122,6 @@ const executes = (view, input, needs) => {
|
|
|
1988
2122
|
};
|
|
1989
2123
|
const single = (view, needs = {}) => pipe(input => executes(view, input, needs), emptyDelta(), concatDelta, emptyDelta);
|
|
1990
2124
|
|
|
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
2125
|
exports.$accumulator = $accumulator;
|
|
2035
2126
|
exports.$and = $and;
|
|
2036
2127
|
exports.$countDict = $countDict;
|
|
@@ -2155,6 +2246,7 @@ exports.range = range;
|
|
|
2155
2246
|
exports.regex = regex;
|
|
2156
2247
|
exports.root = root;
|
|
2157
2248
|
exports.set = set;
|
|
2249
|
+
exports.setF = setF;
|
|
2158
2250
|
exports.setField = setField;
|
|
2159
2251
|
exports.single = single;
|
|
2160
2252
|
exports.size = size;
|