@omegup/msync 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.esm.js +22 -34
  2. package/index.js +21 -33
  3. package/package.json +1 -1
package/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Timestamp, UUID, MongoClient } from 'mongodb';
1
+ import { UUID, Timestamp, MongoClient } from 'mongodb';
2
2
  import crypto$1 from 'crypto';
3
3
  import { canonicalize } from 'json-canonicalize';
4
4
  import { SynchronousPromise } from 'synchronous-promise';
@@ -82,7 +82,7 @@ const val = (val) => asExpr({
82
82
  : val),
83
83
  });
84
84
  const current = asExpr({
85
- raw: () => asExprRaw(new Timestamp(0xffffffffffffffffn)),
85
+ raw: () => asExprRaw('$$CLUSTER_TIME'),
86
86
  });
87
87
  const $let = (vars, inExpr) => asExpr({
88
88
  raw: f => asExprRaw({
@@ -918,6 +918,7 @@ const subMerge = (args, out, gid, extra, idPrefix, first) => {
918
918
  };
919
919
  const addTSAndExtra = {
920
920
  ...mapExact0(e, to),
921
+ ...(out.whenNotMatched === 'insert' ? { deletedAt: ['deletedAt', to(nil)] } : {}),
921
922
  touchedAt: ['touchedAt', to(current)],
922
923
  };
923
924
  const updater = set()(addTSAndExtra);
@@ -1280,7 +1281,8 @@ const createIndexWithRetry = async (collection, indexSpec, options) => {
1280
1281
  }
1281
1282
  catch (e) {
1282
1283
  if ([85, 276].includes(e.code)) {
1283
- log('Index created with different name', e.code, { collection: collection.collectionName});
1284
+ log('Index created with different name', e.code, {
1285
+ collection: collection.collectionName});
1284
1286
  break;
1285
1287
  }
1286
1288
  if (e.code == 12587) {
@@ -1295,6 +1297,11 @@ const createIndexWithRetry = async (collection, indexSpec, options) => {
1295
1297
  break;
1296
1298
  }
1297
1299
  };
1300
+ const ensureCollection = async (db, collectionName) => {
1301
+ if (!(await db.listCollections({ name: collectionName }, { nameOnly: true }).next())) {
1302
+ await db.createCollection(collectionName);
1303
+ }
1304
+ };
1298
1305
 
1299
1306
  const patch = ({ ...x }, k, v) => {
1300
1307
  delete x[k];
@@ -1600,7 +1607,6 @@ const addTeardown = (it, tr) => {
1600
1607
 
1601
1608
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
1602
1609
  const maxTimestamp = new Timestamp(0xffffffffffffffffn);
1603
- const isMax = (x) => x instanceof Timestamp && x.equals(maxTimestamp);
1604
1610
  const getCurrentTimestamp = async (db) => {
1605
1611
  const adminDb = db.admin();
1606
1612
  const serverStatus = await adminDb.command({ serverStatus: 1 });
@@ -1623,9 +1629,9 @@ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
1623
1629
  await sleep(pollMs);
1624
1630
  }
1625
1631
  }
1626
- async function* tailOplog(db, opts) {
1627
- let lastTs = opts.since ?? (await getCurrentTimestamp(db));
1628
- const reopenDelayMs = opts.reopenDelayMs ?? 250;
1632
+ async function* tailOplog(db) {
1633
+ let lastTs = await getCurrentTimestamp(db);
1634
+ const reopenDelayMs = 250;
1629
1635
  const coll = db.client.db('local').collection('oplog.rs');
1630
1636
  while (true) {
1631
1637
  const cursor = coll.find({
@@ -1639,14 +1645,12 @@ async function* tailOplog(db, opts) {
1639
1645
  });
1640
1646
  try {
1641
1647
  for await (const doc of cursor) {
1642
- lastTs = doc.ts;
1643
1648
  if (doc.op === 'i' || '_id' in doc.o) {
1644
1649
  const fields = new Set(Object.keys(doc.o));
1645
1650
  fields.delete('_id');
1646
- yield { fields, doc, changeTouched: isMax(doc.o['touchedAt']) };
1651
+ yield { fields, doc };
1647
1652
  }
1648
1653
  else {
1649
- let changeTouched = false;
1650
1654
  if (doc.o['$v'] !== 2) {
1651
1655
  throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc)}`);
1652
1656
  }
@@ -1655,21 +1659,19 @@ async function* tailOplog(db, opts) {
1655
1659
  for (const updateOp in diff) {
1656
1660
  if (['u', 'i', 'd'].includes(updateOp)) {
1657
1661
  updatedFields.push(...Object.keys(diff[updateOp]));
1658
- if (isMax(diff[updateOp]['touchedAt'])) {
1659
- changeTouched = true;
1660
- }
1661
1662
  }
1662
1663
  else if (updateOp.startsWith('s')) {
1663
1664
  updatedFields.push(updateOp.slice(1));
1664
1665
  }
1665
1666
  }
1666
- yield { fields: new Set(updatedFields), doc, changeTouched };
1667
+ yield { fields: new Set(updatedFields), doc };
1667
1668
  }
1668
1669
  }
1669
1670
  }
1670
1671
  catch (e) {
1671
1672
  log('oplog loop error, notifying watchers and reopening');
1672
1673
  console.error(e);
1674
+ lastTs = await getCurrentTimestamp(db);
1673
1675
  yield null;
1674
1676
  }
1675
1677
  finally {
@@ -1691,7 +1693,7 @@ const loop = async (db) => {
1691
1693
  let notify = makePromise();
1692
1694
  let batch = [];
1693
1695
  const run = async () => {
1694
- for await (const event of tailOplog(db, {})) {
1696
+ for await (const event of tailOplog(db)) {
1695
1697
  if (event?.fields.size === 0)
1696
1698
  continue;
1697
1699
  batch = event && batch ? [...batch, event] : null;
@@ -1718,23 +1720,6 @@ const loop = async (db) => {
1718
1720
  }
1719
1721
  continue;
1720
1722
  }
1721
- const groups = Object.groupBy(events.filter(e => e.changeTouched), ev => ev.doc.ns);
1722
- for (const [ns, evs] of Object.entries(groups)) {
1723
- if (!evs)
1724
- continue;
1725
- const [dbName, collName] = ns.split('.');
1726
- if (dbName !== db.databaseName)
1727
- continue;
1728
- const coll = db.collection(collName);
1729
- coll
1730
- .bulkWrite(evs.map((e) => ({
1731
- updateOne: {
1732
- filter: { _id: e.doc.o['_id'] ?? e.doc.o2?._id },
1733
- update: { $set: { touchedAt: e.doc.ts } },
1734
- },
1735
- })))
1736
- .catch(() => { });
1737
- }
1738
1723
  for (const { fields, doc } of events) {
1739
1724
  const m = watchers.get(doc.ns);
1740
1725
  if (!m)
@@ -1802,6 +1787,7 @@ const actions = {
1802
1787
  ],
1803
1788
  };
1804
1789
 
1790
+ const previous = (ts) => new Timestamp({ t: ts.high - 60, i: 0 });
1805
1791
  const getFirstStages = (view, needs) => {
1806
1792
  const { projection, hardMatch: pre, match } = view;
1807
1793
  const projectInput = projection && $project_(spread(projection, {
@@ -1812,7 +1798,7 @@ const getFirstStages = (view, needs) => {
1812
1798
  const hardMatch = removeNotYetSynchronizedFields ? $and(pre, ...removeNotYetSynchronizedFields) : pre;
1813
1799
  const firstStages = (lastTS, keepNulls = false) => {
1814
1800
  const hardQuery = $and(lastTS
1815
- ? root().of('touchedAt').has($gtTs(lastTS.ts))
1801
+ ? root().of('touchedAt').has($gtTs(previous(lastTS.ts)))
1816
1802
  : root().of('deletedAt').has($eq(null)), lastTS ? null : match && $expr(match), keepNulls ? pre : hardMatch);
1817
1803
  const ln = link()
1818
1804
  .with($match_(hardQuery));
@@ -1967,6 +1953,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1967
1953
  partialFilterExpression: { updated: true, after: null, before: null },
1968
1954
  name: 'updated_nulls_' + new UUID().toString('base64'),
1969
1955
  });
1956
+ await ensureCollection(db, coll);
1970
1957
  await db.command({
1971
1958
  collMod: coll,
1972
1959
  changeStreamPreAndPostImages: { enabled: true },
@@ -2118,7 +2105,7 @@ const executes$1 = (view, input, streamName, needs) => {
2118
2105
  }));
2119
2106
  const notDeleted = root().of('deletedAt').has($eq(null));
2120
2107
  const stages = (lastTS) => {
2121
- const hardQuery = $and(lastTS && root().of('touchedAt').has($gteTs(lastTS.ts)), hardMatch, notDeleted, match && $expr(match));
2108
+ const hardQuery = $and(lastTS && root().of('touchedAt').has($gteTs(previous(lastTS.ts))), hardMatch, notDeleted, match && $expr(match));
2122
2109
  const ln = link().with($match_(hardQuery));
2123
2110
  return (projectInput ? ln.with(projectInput) : ln).with(input);
2124
2111
  };
@@ -2148,6 +2135,7 @@ const executes$1 = (view, input, streamName, needs) => {
2148
2135
  const stop = withStop(step0);
2149
2136
  const step1 = async () => {
2150
2137
  log('creating indexes');
2138
+ await ensureCollection(db, coll);
2151
2139
  await db.command({
2152
2140
  collMod: coll,
2153
2141
  changeStreamPreAndPostImages: { enabled: true },
package/index.js CHANGED
@@ -84,7 +84,7 @@ const val = (val) => asExpr({
84
84
  : val),
85
85
  });
86
86
  const current = asExpr({
87
- raw: () => asExprRaw(new mongodb.Timestamp(0xffffffffffffffffn)),
87
+ raw: () => asExprRaw('$$CLUSTER_TIME'),
88
88
  });
89
89
  const $let = (vars, inExpr) => asExpr({
90
90
  raw: f => asExprRaw({
@@ -920,6 +920,7 @@ const subMerge = (args, out, gid, extra, idPrefix, first) => {
920
920
  };
921
921
  const addTSAndExtra = {
922
922
  ...mapExact0(e, to),
923
+ ...(out.whenNotMatched === 'insert' ? { deletedAt: ['deletedAt', to(nil)] } : {}),
923
924
  touchedAt: ['touchedAt', to(current)],
924
925
  };
925
926
  const updater = set()(addTSAndExtra);
@@ -1282,7 +1283,8 @@ const createIndexWithRetry = async (collection, indexSpec, options) => {
1282
1283
  }
1283
1284
  catch (e) {
1284
1285
  if ([85, 276].includes(e.code)) {
1285
- log('Index created with different name', e.code, { collection: collection.collectionName});
1286
+ log('Index created with different name', e.code, {
1287
+ collection: collection.collectionName});
1286
1288
  break;
1287
1289
  }
1288
1290
  if (e.code == 12587) {
@@ -1297,6 +1299,11 @@ const createIndexWithRetry = async (collection, indexSpec, options) => {
1297
1299
  break;
1298
1300
  }
1299
1301
  };
1302
+ const ensureCollection = async (db, collectionName) => {
1303
+ if (!(await db.listCollections({ name: collectionName }, { nameOnly: true }).next())) {
1304
+ await db.createCollection(collectionName);
1305
+ }
1306
+ };
1300
1307
 
1301
1308
  const patch = ({ ...x }, k, v) => {
1302
1309
  delete x[k];
@@ -1602,7 +1609,6 @@ const addTeardown = (it, tr) => {
1602
1609
 
1603
1610
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
1604
1611
  const maxTimestamp = new mongodb.Timestamp(0xffffffffffffffffn);
1605
- const isMax = (x) => x instanceof mongodb.Timestamp && x.equals(maxTimestamp);
1606
1612
  const getCurrentTimestamp = async (db) => {
1607
1613
  const adminDb = db.admin();
1608
1614
  const serverStatus = await adminDb.command({ serverStatus: 1 });
@@ -1625,9 +1631,9 @@ async function waitUntilStablePast(db, oplogTs, { pollMs = 0, timeoutMs = 10_000
1625
1631
  await sleep(pollMs);
1626
1632
  }
1627
1633
  }
1628
- async function* tailOplog(db, opts) {
1629
- let lastTs = opts.since ?? (await getCurrentTimestamp(db));
1630
- const reopenDelayMs = opts.reopenDelayMs ?? 250;
1634
+ async function* tailOplog(db) {
1635
+ let lastTs = await getCurrentTimestamp(db);
1636
+ const reopenDelayMs = 250;
1631
1637
  const coll = db.client.db('local').collection('oplog.rs');
1632
1638
  while (true) {
1633
1639
  const cursor = coll.find({
@@ -1641,14 +1647,12 @@ async function* tailOplog(db, opts) {
1641
1647
  });
1642
1648
  try {
1643
1649
  for await (const doc of cursor) {
1644
- lastTs = doc.ts;
1645
1650
  if (doc.op === 'i' || '_id' in doc.o) {
1646
1651
  const fields = new Set(Object.keys(doc.o));
1647
1652
  fields.delete('_id');
1648
- yield { fields, doc, changeTouched: isMax(doc.o['touchedAt']) };
1653
+ yield { fields, doc };
1649
1654
  }
1650
1655
  else {
1651
- let changeTouched = false;
1652
1656
  if (doc.o['$v'] !== 2) {
1653
1657
  throw new Error(`Expected update with $v: 2, got ${JSON.stringify(doc)}`);
1654
1658
  }
@@ -1657,21 +1661,19 @@ async function* tailOplog(db, opts) {
1657
1661
  for (const updateOp in diff) {
1658
1662
  if (['u', 'i', 'd'].includes(updateOp)) {
1659
1663
  updatedFields.push(...Object.keys(diff[updateOp]));
1660
- if (isMax(diff[updateOp]['touchedAt'])) {
1661
- changeTouched = true;
1662
- }
1663
1664
  }
1664
1665
  else if (updateOp.startsWith('s')) {
1665
1666
  updatedFields.push(updateOp.slice(1));
1666
1667
  }
1667
1668
  }
1668
- yield { fields: new Set(updatedFields), doc, changeTouched };
1669
+ yield { fields: new Set(updatedFields), doc };
1669
1670
  }
1670
1671
  }
1671
1672
  }
1672
1673
  catch (e) {
1673
1674
  log('oplog loop error, notifying watchers and reopening');
1674
1675
  console.error(e);
1676
+ lastTs = await getCurrentTimestamp(db);
1675
1677
  yield null;
1676
1678
  }
1677
1679
  finally {
@@ -1693,7 +1695,7 @@ const loop = async (db) => {
1693
1695
  let notify = makePromise();
1694
1696
  let batch = [];
1695
1697
  const run = async () => {
1696
- for await (const event of tailOplog(db, {})) {
1698
+ for await (const event of tailOplog(db)) {
1697
1699
  if (event?.fields.size === 0)
1698
1700
  continue;
1699
1701
  batch = event && batch ? [...batch, event] : null;
@@ -1720,23 +1722,6 @@ const loop = async (db) => {
1720
1722
  }
1721
1723
  continue;
1722
1724
  }
1723
- const groups = Object.groupBy(events.filter(e => e.changeTouched), ev => ev.doc.ns);
1724
- for (const [ns, evs] of Object.entries(groups)) {
1725
- if (!evs)
1726
- continue;
1727
- const [dbName, collName] = ns.split('.');
1728
- if (dbName !== db.databaseName)
1729
- continue;
1730
- const coll = db.collection(collName);
1731
- coll
1732
- .bulkWrite(evs.map((e) => ({
1733
- updateOne: {
1734
- filter: { _id: e.doc.o['_id'] ?? e.doc.o2?._id },
1735
- update: { $set: { touchedAt: e.doc.ts } },
1736
- },
1737
- })))
1738
- .catch(() => { });
1739
- }
1740
1725
  for (const { fields, doc } of events) {
1741
1726
  const m = watchers.get(doc.ns);
1742
1727
  if (!m)
@@ -1804,6 +1789,7 @@ const actions = {
1804
1789
  ],
1805
1790
  };
1806
1791
 
1792
+ const previous = (ts) => new mongodb.Timestamp({ t: ts.high - 60, i: 0 });
1807
1793
  const getFirstStages = (view, needs) => {
1808
1794
  const { projection, hardMatch: pre, match } = view;
1809
1795
  const projectInput = projection && $project_(spread(projection, {
@@ -1814,7 +1800,7 @@ const getFirstStages = (view, needs) => {
1814
1800
  const hardMatch = removeNotYetSynchronizedFields ? $and(pre, ...removeNotYetSynchronizedFields) : pre;
1815
1801
  const firstStages = (lastTS, keepNulls = false) => {
1816
1802
  const hardQuery = $and(lastTS
1817
- ? root().of('touchedAt').has($gtTs(lastTS.ts))
1803
+ ? root().of('touchedAt').has($gtTs(previous(lastTS.ts)))
1818
1804
  : root().of('deletedAt').has($eq(null)), lastTS ? null : match && $expr(match), keepNulls ? pre : hardMatch);
1819
1805
  const ln = link()
1820
1806
  .with($match_(hardQuery));
@@ -1969,6 +1955,7 @@ const executes$2 = (view, input, streamName, skip = false, after, needs = {}) =>
1969
1955
  partialFilterExpression: { updated: true, after: null, before: null },
1970
1956
  name: 'updated_nulls_' + new mongodb.UUID().toString('base64'),
1971
1957
  });
1958
+ await ensureCollection(db, coll);
1972
1959
  await db.command({
1973
1960
  collMod: coll,
1974
1961
  changeStreamPreAndPostImages: { enabled: true },
@@ -2120,7 +2107,7 @@ const executes$1 = (view, input, streamName, needs) => {
2120
2107
  }));
2121
2108
  const notDeleted = root().of('deletedAt').has($eq(null));
2122
2109
  const stages = (lastTS) => {
2123
- const hardQuery = $and(lastTS && root().of('touchedAt').has($gteTs(lastTS.ts)), hardMatch, notDeleted, match && $expr(match));
2110
+ const hardQuery = $and(lastTS && root().of('touchedAt').has($gteTs(previous(lastTS.ts))), hardMatch, notDeleted, match && $expr(match));
2124
2111
  const ln = link().with($match_(hardQuery));
2125
2112
  return (projectInput ? ln.with(projectInput) : ln).with(input);
2126
2113
  };
@@ -2150,6 +2137,7 @@ const executes$1 = (view, input, streamName, needs) => {
2150
2137
  const stop = withStop(step0);
2151
2138
  const step1 = async () => {
2152
2139
  log('creating indexes');
2140
+ await ensureCollection(db, coll);
2153
2141
  await db.command({
2154
2142
  collMod: coll,
2155
2143
  changeStreamPreAndPostImages: { enabled: true },
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "index.esm.js",
4
4
  "typings": "index.d.ts",
5
5
  "name": "@omegup/msync",
6
- "version": "0.1.28",
6
+ "version": "0.1.30",
7
7
  "dependencies": {
8
8
  "dayjs": "^1.11.9",
9
9
  "dotenv": "^16.3.1",