@taladb/sync-mongodb 0.9.2 → 0.9.3

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/dist/index.d.mts CHANGED
@@ -4,8 +4,10 @@ import { Collection } from 'mongodb';
4
4
  interface MongoSyncAdapterOptions {
5
5
  /**
6
6
  * The MongoDB collection to use as the change store. Create it from your own
7
- * `MongoClient` so the app owns the connection lifecycle. Index `changed_at`
8
- * for pull performance: `await collection.createIndex({ changed_at: 1 })`.
7
+ * `MongoClient` so the app owns the connection lifecycle. Index both fields
8
+ * the adapter queries on:
9
+ * `await collection.createIndex({ changed_at: 1 })` (pull) and
10
+ * `await collection.createIndex({ doc_key: 1 })` (push).
9
11
  */
10
12
  collection: Collection;
11
13
  }
@@ -31,7 +33,8 @@ declare class MongoSyncAdapter implements SyncAdapter {
31
33
  constructor(options: MongoSyncAdapterOptions);
32
34
  /**
33
35
  * Convenience: open a MongoDB connection from a URI and return a ready
34
- * adapter plus a `close()` to release it. Creates the `changed_at` index.
36
+ * adapter plus a `close()` to release it. Creates the indexes the adapter
37
+ * queries on.
35
38
  *
36
39
  * ```ts
37
40
  * const { adapter, close } = await MongoSyncAdapter.connect({
package/dist/index.d.ts CHANGED
@@ -4,8 +4,10 @@ import { Collection } from 'mongodb';
4
4
  interface MongoSyncAdapterOptions {
5
5
  /**
6
6
  * The MongoDB collection to use as the change store. Create it from your own
7
- * `MongoClient` so the app owns the connection lifecycle. Index `changed_at`
8
- * for pull performance: `await collection.createIndex({ changed_at: 1 })`.
7
+ * `MongoClient` so the app owns the connection lifecycle. Index both fields
8
+ * the adapter queries on:
9
+ * `await collection.createIndex({ changed_at: 1 })` (pull) and
10
+ * `await collection.createIndex({ doc_key: 1 })` (push).
9
11
  */
10
12
  collection: Collection;
11
13
  }
@@ -31,7 +33,8 @@ declare class MongoSyncAdapter implements SyncAdapter {
31
33
  constructor(options: MongoSyncAdapterOptions);
32
34
  /**
33
35
  * Convenience: open a MongoDB connection from a URI and return a ready
34
- * adapter plus a `close()` to release it. Creates the `changed_at` index.
36
+ * adapter plus a `close()` to release it. Creates the indexes the adapter
37
+ * queries on.
35
38
  *
36
39
  * ```ts
37
40
  * const { adapter, close } = await MongoSyncAdapter.connect({
package/dist/index.js CHANGED
@@ -33,13 +33,15 @@ __export(index_exports, {
33
33
  MongoSyncAdapter: () => MongoSyncAdapter
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
+ var import_node_crypto = require("crypto");
36
37
  var MongoSyncAdapter = class _MongoSyncAdapter {
37
38
  constructor(options) {
38
39
  this.store = options.collection;
39
40
  }
40
41
  /**
41
42
  * Convenience: open a MongoDB connection from a URI and return a ready
42
- * adapter plus a `close()` to release it. Creates the `changed_at` index.
43
+ * adapter plus a `close()` to release it. Creates the indexes the adapter
44
+ * queries on.
43
45
  *
44
46
  * ```ts
45
47
  * const { adapter, close } = await MongoSyncAdapter.connect({
@@ -55,6 +57,7 @@ var MongoSyncAdapter = class _MongoSyncAdapter {
55
57
  await client.connect();
56
58
  const store = client.db(opts.db).collection(opts.collection ?? "taladb_changes");
57
59
  await store.createIndex({ changed_at: 1 });
60
+ await store.createIndex({ doc_key: 1 });
58
61
  return {
59
62
  adapter: new _MongoSyncAdapter({ collection: store }),
60
63
  close: () => client.close()
@@ -63,27 +66,37 @@ var MongoSyncAdapter = class _MongoSyncAdapter {
63
66
  async push(changeset) {
64
67
  const changes = JSON.parse(changeset);
65
68
  if (changes.length === 0) return;
66
- const ops = changes.map((chg) => {
67
- const key = `${chg.collection}::${chg.id}`;
69
+ const docKeyOf = (chg) => `${chg.collection}::${chg.id}`;
70
+ const stored = await this.store.find(
71
+ { doc_key: { $in: [...new Set(changes.map(docKeyOf))] } },
72
+ { projection: { doc_key: 1, changed_at: 1 } }
73
+ ).toArray();
74
+ const storedMax = /* @__PURE__ */ new Map();
75
+ for (const row of stored) {
76
+ storedMax.set(row.doc_key, Math.max(storedMax.get(row.doc_key) ?? -1, row.changed_at));
77
+ }
78
+ const winning = new Map(storedMax);
79
+ for (const chg of changes) {
80
+ const key = docKeyOf(chg);
81
+ winning.set(key, Math.max(winning.get(key) ?? -1, chg.changed_at));
82
+ }
83
+ const inserts = changes.filter((chg) => chg.changed_at === winning.get(docKeyOf(chg))).map((chg) => {
84
+ const change = JSON.stringify(chg);
85
+ const docKey = docKeyOf(chg);
86
+ const digest = (0, import_node_crypto.createHash)("sha256").update(change).digest("hex").slice(0, 16);
68
87
  return {
69
88
  updateOne: {
70
- filter: { _id: key },
71
- update: [
72
- {
73
- $replaceWith: {
74
- $cond: [
75
- { $gt: [chg.changed_at, { $ifNull: ["$changed_at", -1] }] },
76
- { _id: key, changed_at: chg.changed_at, change: JSON.stringify(chg) },
77
- "$$ROOT"
78
- ]
79
- }
80
- }
81
- ],
89
+ filter: { _id: `${docKey}::${chg.changed_at}::${digest}` },
90
+ update: { $setOnInsert: { doc_key: docKey, changed_at: chg.changed_at, change } },
82
91
  upsert: true
83
92
  }
84
93
  };
85
94
  });
86
- await this.store.bulkWrite(ops, { ordered: false });
95
+ const prunes = [...winning].filter(([key, ts]) => stored.some((r) => r.doc_key === key && r.changed_at < ts)).map(([key, ts]) => ({
96
+ deleteMany: { filter: { doc_key: key, changed_at: { $lt: ts } } }
97
+ }));
98
+ if (inserts.length > 0) await this.store.bulkWrite(inserts, { ordered: false });
99
+ if (prunes.length > 0) await this.store.bulkWrite(prunes, { ordered: false });
87
100
  }
88
101
  async pull(sinceMs) {
89
102
  const rows = await this.store.find({ changed_at: { $gt: sinceMs } }).sort({ changed_at: 1 }).toArray();
package/dist/index.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  // src/index.ts
2
+ import { createHash } from "crypto";
2
3
  var MongoSyncAdapter = class _MongoSyncAdapter {
3
4
  constructor(options) {
4
5
  this.store = options.collection;
5
6
  }
6
7
  /**
7
8
  * Convenience: open a MongoDB connection from a URI and return a ready
8
- * adapter plus a `close()` to release it. Creates the `changed_at` index.
9
+ * adapter plus a `close()` to release it. Creates the indexes the adapter
10
+ * queries on.
9
11
  *
10
12
  * ```ts
11
13
  * const { adapter, close } = await MongoSyncAdapter.connect({
@@ -21,6 +23,7 @@ var MongoSyncAdapter = class _MongoSyncAdapter {
21
23
  await client.connect();
22
24
  const store = client.db(opts.db).collection(opts.collection ?? "taladb_changes");
23
25
  await store.createIndex({ changed_at: 1 });
26
+ await store.createIndex({ doc_key: 1 });
24
27
  return {
25
28
  adapter: new _MongoSyncAdapter({ collection: store }),
26
29
  close: () => client.close()
@@ -29,27 +32,37 @@ var MongoSyncAdapter = class _MongoSyncAdapter {
29
32
  async push(changeset) {
30
33
  const changes = JSON.parse(changeset);
31
34
  if (changes.length === 0) return;
32
- const ops = changes.map((chg) => {
33
- const key = `${chg.collection}::${chg.id}`;
35
+ const docKeyOf = (chg) => `${chg.collection}::${chg.id}`;
36
+ const stored = await this.store.find(
37
+ { doc_key: { $in: [...new Set(changes.map(docKeyOf))] } },
38
+ { projection: { doc_key: 1, changed_at: 1 } }
39
+ ).toArray();
40
+ const storedMax = /* @__PURE__ */ new Map();
41
+ for (const row of stored) {
42
+ storedMax.set(row.doc_key, Math.max(storedMax.get(row.doc_key) ?? -1, row.changed_at));
43
+ }
44
+ const winning = new Map(storedMax);
45
+ for (const chg of changes) {
46
+ const key = docKeyOf(chg);
47
+ winning.set(key, Math.max(winning.get(key) ?? -1, chg.changed_at));
48
+ }
49
+ const inserts = changes.filter((chg) => chg.changed_at === winning.get(docKeyOf(chg))).map((chg) => {
50
+ const change = JSON.stringify(chg);
51
+ const docKey = docKeyOf(chg);
52
+ const digest = createHash("sha256").update(change).digest("hex").slice(0, 16);
34
53
  return {
35
54
  updateOne: {
36
- filter: { _id: key },
37
- update: [
38
- {
39
- $replaceWith: {
40
- $cond: [
41
- { $gt: [chg.changed_at, { $ifNull: ["$changed_at", -1] }] },
42
- { _id: key, changed_at: chg.changed_at, change: JSON.stringify(chg) },
43
- "$$ROOT"
44
- ]
45
- }
46
- }
47
- ],
55
+ filter: { _id: `${docKey}::${chg.changed_at}::${digest}` },
56
+ update: { $setOnInsert: { doc_key: docKey, changed_at: chg.changed_at, change } },
48
57
  upsert: true
49
58
  }
50
59
  };
51
60
  });
52
- await this.store.bulkWrite(ops, { ordered: false });
61
+ const prunes = [...winning].filter(([key, ts]) => stored.some((r) => r.doc_key === key && r.changed_at < ts)).map(([key, ts]) => ({
62
+ deleteMany: { filter: { doc_key: key, changed_at: { $lt: ts } } }
63
+ }));
64
+ if (inserts.length > 0) await this.store.bulkWrite(inserts, { ordered: false });
65
+ if (prunes.length > 0) await this.store.bulkWrite(prunes, { ordered: false });
53
66
  }
54
67
  async pull(sinceMs) {
55
68
  const rows = await this.store.find({ changed_at: { $gt: sinceMs } }).sort({ changed_at: 1 }).toArray();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/sync-mongodb",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "MongoDB bidirectional sync adapter for TalaDB — sync a local TalaDB with a MongoDB collection, no intermediate API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "mongodb": ">=5.0.0",
42
- "taladb": "^0.9.2"
42
+ "taladb": "^0.9.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.0.0",
@@ -47,7 +47,7 @@
47
47
  "tsup": "^8.0.0",
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^3.2.0",
50
- "taladb": "0.9.2"
50
+ "taladb": "0.9.3"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup",