@semics-tech/mongolite 1.3.0 → 1.4.0

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 (63) hide show
  1. package/README.md +79 -21
  2. package/dist/cjs/collection.js +8 -1
  3. package/dist/cjs/index.js +108 -1
  4. package/dist/cjs/mongo-client.js +18 -1
  5. package/dist/cjs/server.js +164 -0
  6. package/dist/cjs/sync/commands.js +126 -0
  7. package/dist/cjs/sync/diff.js +174 -0
  8. package/dist/cjs/sync/httpSink.js +159 -0
  9. package/dist/cjs/sync/index.js +40 -0
  10. package/dist/cjs/sync/mongoExecutor.js +306 -0
  11. package/dist/cjs/sync/mongoSink.js +161 -0
  12. package/dist/cjs/sync/outbox.js +358 -0
  13. package/dist/cjs/sync/protocol.js +177 -0
  14. package/dist/cjs/sync/replicator.js +747 -0
  15. package/dist/cjs/sync/shadow.js +178 -0
  16. package/dist/cjs/sync/types.js +2 -0
  17. package/dist/cloudflare.d.ts +1 -1
  18. package/dist/cloudflare.js.map +1 -1
  19. package/dist/collection.js +8 -1
  20. package/dist/collection.js.map +1 -1
  21. package/dist/index.d.ts +68 -2
  22. package/dist/index.js +96 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/mongo-client.d.ts +13 -0
  25. package/dist/mongo-client.js +18 -1
  26. package/dist/mongo-client.js.map +1 -1
  27. package/dist/server.d.ts +97 -0
  28. package/dist/server.js +158 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/sync/commands.d.ts +49 -0
  31. package/dist/sync/commands.js +118 -0
  32. package/dist/sync/commands.js.map +1 -0
  33. package/dist/sync/diff.d.ts +55 -0
  34. package/dist/sync/diff.js +169 -0
  35. package/dist/sync/diff.js.map +1 -0
  36. package/dist/sync/httpSink.d.ts +93 -0
  37. package/dist/sync/httpSink.js +155 -0
  38. package/dist/sync/httpSink.js.map +1 -0
  39. package/dist/sync/index.d.ts +17 -0
  40. package/dist/sync/index.js +10 -0
  41. package/dist/sync/index.js.map +1 -0
  42. package/dist/sync/mongoExecutor.d.ts +37 -0
  43. package/dist/sync/mongoExecutor.js +302 -0
  44. package/dist/sync/mongoExecutor.js.map +1 -0
  45. package/dist/sync/mongoSink.d.ts +109 -0
  46. package/dist/sync/mongoSink.js +123 -0
  47. package/dist/sync/mongoSink.js.map +1 -0
  48. package/dist/sync/outbox.d.ts +120 -0
  49. package/dist/sync/outbox.js +355 -0
  50. package/dist/sync/outbox.js.map +1 -0
  51. package/dist/sync/protocol.d.ts +93 -0
  52. package/dist/sync/protocol.js +168 -0
  53. package/dist/sync/protocol.js.map +1 -0
  54. package/dist/sync/replicator.d.ts +177 -0
  55. package/dist/sync/replicator.js +744 -0
  56. package/dist/sync/replicator.js.map +1 -0
  57. package/dist/sync/shadow.d.ts +54 -0
  58. package/dist/sync/shadow.js +173 -0
  59. package/dist/sync/shadow.js.map +1 -0
  60. package/dist/sync/types.d.ts +388 -0
  61. package/dist/sync/types.js +2 -0
  62. package/dist/sync/types.js.map +1 -0
  63. package/package.json +15 -1
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The durable change log ("outbox") that backs MongoDB replication.
3
+ *
4
+ * SQLite triggers append a row here inside the very same transaction as the
5
+ * write that caused it, so a change is either committed locally *and* queued for
6
+ * replication, or neither. Replicators then drain the log at their own pace and
7
+ * record a checkpoint; nothing is dropped because the process crashed or the
8
+ * upstream was unreachable.
9
+ */
10
+ import type { IDatabaseAdapter } from '../db.js';
11
+ import type { SyncDeadLetter, SyncOutboxRecord } from './types.js';
12
+ export declare const OUTBOX_TABLE = "__mongolite_sync_outbox__";
13
+ export declare const STATE_TABLE = "__mongolite_sync_state__";
14
+ export declare const DEAD_LETTER_TABLE = "__mongolite_sync_deadletter__";
15
+ /** Prefix shared by every table and trigger this module creates. */
16
+ export declare const SYNC_OBJECT_PREFIX = "__mongolite_sync";
17
+ /**
18
+ * Owns the outbox tables, the capture triggers, and every replicator's
19
+ * checkpoint. A single outbox serves any number of replicators: each keeps its
20
+ * own checkpoint row, and rows are pruned only once *every* replicator has
21
+ * passed them.
22
+ */
23
+ export declare class SyncOutbox {
24
+ private readonly db;
25
+ private schemaReady;
26
+ private readonly installedTriggers;
27
+ constructor(db: IDatabaseAdapter);
28
+ /** Creates the outbox, checkpoint and dead-letter tables. Idempotent. */
29
+ ensureSchema(): Promise<void>;
30
+ private createSchema;
31
+ /**
32
+ * Installs the insert/update/delete capture triggers for a collection.
33
+ *
34
+ * Triggers deliberately outlive the replicator that created them: changes made
35
+ * while replication is stopped still need to be captured, or restarting would
36
+ * silently skip them. Use {@link SyncOutbox.removeTriggers} to opt out.
37
+ */
38
+ ensureTriggers(collection: string): Promise<void>;
39
+ /**
40
+ * Removes the capture triggers for a collection. Changes to it stop being
41
+ * recorded — existing queued rows are untouched.
42
+ */
43
+ removeTriggers(collection: string): Promise<void>;
44
+ /** Lists user collections, excluding MongoLite's own bookkeeping tables. */
45
+ listCollections(): Promise<string[]>;
46
+ /**
47
+ * Reconciles the in-memory "already installed" cache with what is actually in
48
+ * the schema.
49
+ *
50
+ * Dropping a table drops its triggers with it, so a collection that was
51
+ * dropped and recreated would otherwise be silently uncaptured — the cache
52
+ * would insist its triggers were still there.
53
+ */
54
+ refreshTriggerCache(): Promise<void>;
55
+ /** The newest outbox id, or 0 when the log is empty. */
56
+ currentSequence(): Promise<number>;
57
+ /** Reads the persisted checkpoint for a replicator, creating its state row if absent. */
58
+ loadState(name: string): Promise<{
59
+ checkpoint: number;
60
+ backfilled: string[];
61
+ }>;
62
+ /**
63
+ * Advances a replicator's checkpoint.
64
+ *
65
+ * This is the acknowledgement, and it is a single atomic statement: a crash
66
+ * either leaves the old checkpoint (the batch is replayed, which is safe
67
+ * because upstream writes are idempotent) or the new one.
68
+ */
69
+ saveCheckpoint(name: string, checkpoint: number): Promise<void>;
70
+ /** Records that a collection's existing contents have been backfilled. */
71
+ markBackfilled(name: string, collections: string[]): Promise<void>;
72
+ /** Forgets a replicator, releasing the outbox rows its checkpoint was pinning. */
73
+ unregister(name: string): Promise<void>;
74
+ /** Reads the next batch of changes after `checkpoint`, oldest first. */
75
+ readBatch(checkpoint: number, limit: number, collections: string[] | null): Promise<SyncOutboxRecord[]>;
76
+ /** Count of rows a replicator still has to apply. */
77
+ pendingCount(checkpoint: number, collections: string[] | null): Promise<number>;
78
+ /**
79
+ * Deletes rows every registered replicator has already applied.
80
+ * @returns Number of rows removed.
81
+ */
82
+ prune(): Promise<number>;
83
+ /**
84
+ * Drops superseded revisions, keeping only the newest row per document.
85
+ *
86
+ * Safe because replication carries whole documents rather than deltas: the
87
+ * newest row alone reproduces the same upstream state. Rows at or below the
88
+ * slowest replicator's checkpoint are left to {@link SyncOutbox.prune}.
89
+ *
90
+ * @returns Number of rows removed.
91
+ */
92
+ compact(): Promise<number>;
93
+ /**
94
+ * Appends a change back onto the log so it is retried on a later drain.
95
+ *
96
+ * Used when a conflict is resolved in favour of the local version: the shadow has
97
+ * since been refreshed, so the replayed push diffs against current upstream state and
98
+ * carries the version it actually lost to.
99
+ */
100
+ requeue(collection: string, documentId: string, document: Record<string, unknown> | null): Promise<void>;
101
+ /** Moves a permanently-rejected change out of the stream so it cannot stall replication. */
102
+ deadLetter(replicator: string, record: {
103
+ collection: string;
104
+ documentId: string;
105
+ operation: string;
106
+ document: unknown;
107
+ }, error: string): Promise<void>;
108
+ /** Reads dead-lettered changes, newest first. */
109
+ listDeadLetters(replicator?: string, limit?: number): Promise<SyncDeadLetter[]>;
110
+ /** Removes dead-lettered rows, e.g. after replaying them by hand. */
111
+ clearDeadLetters(replicator?: string): Promise<number>;
112
+ /**
113
+ * Streams a collection's current contents in `_id` order, for the initial
114
+ * backfill. Keyset pagination keeps memory flat on large collections.
115
+ */
116
+ scanCollection(collection: string, chunkSize: number): AsyncGenerator<Array<{
117
+ documentId: string;
118
+ document: Record<string, unknown> | null;
119
+ }>>;
120
+ }
@@ -0,0 +1,355 @@
1
+ export const OUTBOX_TABLE = '__mongolite_sync_outbox__';
2
+ export const STATE_TABLE = '__mongolite_sync_state__';
3
+ export const DEAD_LETTER_TABLE = '__mongolite_sync_deadletter__';
4
+ /** Prefix shared by every table and trigger this module creates. */
5
+ export const SYNC_OBJECT_PREFIX = '__mongolite_sync';
6
+ /** Quotes a SQL identifier, escaping any embedded double quotes. */
7
+ function quoteIdent(name) {
8
+ return `"${name.replace(/"/g, '""')}"`;
9
+ }
10
+ /** Quotes a SQL string literal, escaping any embedded single quotes. */
11
+ function quoteLiteral(value) {
12
+ return `'${value.replace(/'/g, "''")}'`;
13
+ }
14
+ /** Millisecond epoch as a SQL expression — portable across SQLite builds. */
15
+ const NOW_MS_SQL = `CAST((julianday('now') - 2440587.5) * 86400000.0 AS INTEGER)`;
16
+ function triggerName(collection, operation) {
17
+ return `${SYNC_OBJECT_PREFIX}_${operation}__${collection}`;
18
+ }
19
+ /**
20
+ * Owns the outbox tables, the capture triggers, and every replicator's
21
+ * checkpoint. A single outbox serves any number of replicators: each keeps its
22
+ * own checkpoint row, and rows are pruned only once *every* replicator has
23
+ * passed them.
24
+ */
25
+ export class SyncOutbox {
26
+ constructor(db) {
27
+ this.db = db;
28
+ this.schemaReady = null;
29
+ this.installedTriggers = new Set();
30
+ }
31
+ /** Creates the outbox, checkpoint and dead-letter tables. Idempotent. */
32
+ async ensureSchema() {
33
+ if (!this.schemaReady) {
34
+ this.schemaReady = this.createSchema().catch((err) => {
35
+ // Let a later call retry rather than caching the failure forever.
36
+ this.schemaReady = null;
37
+ throw err;
38
+ });
39
+ }
40
+ return this.schemaReady;
41
+ }
42
+ async createSchema() {
43
+ await this.db.exec(`
44
+ CREATE TABLE IF NOT EXISTS ${OUTBOX_TABLE} (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ collection_name TEXT NOT NULL,
47
+ document_id TEXT NOT NULL,
48
+ operation TEXT NOT NULL,
49
+ full_document TEXT,
50
+ captured_at INTEGER NOT NULL
51
+ );
52
+ `);
53
+ await this.db.exec(`
54
+ CREATE INDEX IF NOT EXISTS ${SYNC_OBJECT_PREFIX}_outbox_key
55
+ ON ${OUTBOX_TABLE} (collection_name, document_id, id);
56
+ `);
57
+ await this.db.exec(`
58
+ CREATE TABLE IF NOT EXISTS ${STATE_TABLE} (
59
+ name TEXT PRIMARY KEY,
60
+ checkpoint INTEGER NOT NULL DEFAULT 0,
61
+ backfilled TEXT NOT NULL DEFAULT '[]',
62
+ updated_at INTEGER NOT NULL DEFAULT 0
63
+ );
64
+ `);
65
+ await this.db.exec(`
66
+ CREATE TABLE IF NOT EXISTS ${DEAD_LETTER_TABLE} (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ replicator TEXT NOT NULL,
69
+ collection_name TEXT NOT NULL,
70
+ document_id TEXT NOT NULL,
71
+ operation TEXT NOT NULL,
72
+ full_document TEXT,
73
+ error TEXT NOT NULL,
74
+ failed_at INTEGER NOT NULL
75
+ );
76
+ `);
77
+ }
78
+ /**
79
+ * Installs the insert/update/delete capture triggers for a collection.
80
+ *
81
+ * Triggers deliberately outlive the replicator that created them: changes made
82
+ * while replication is stopped still need to be captured, or restarting would
83
+ * silently skip them. Use {@link SyncOutbox.removeTriggers} to opt out.
84
+ */
85
+ async ensureTriggers(collection) {
86
+ if (this.installedTriggers.has(collection))
87
+ return;
88
+ await this.ensureSchema();
89
+ const table = quoteIdent(collection);
90
+ const literal = quoteLiteral(collection);
91
+ const insert = (op, row, doc) => `
92
+ CREATE TRIGGER IF NOT EXISTS ${quoteIdent(triggerName(collection, op))}
93
+ AFTER ${op === 'insert' ? 'INSERT' : op === 'update' ? 'UPDATE' : 'DELETE'} ON ${table}
94
+ FOR EACH ROW
95
+ BEGIN
96
+ INSERT INTO ${OUTBOX_TABLE}
97
+ (collection_name, document_id, operation, full_document, captured_at)
98
+ VALUES
99
+ (${literal}, ${row}._id, ${quoteLiteral(op)}, ${doc}, ${NOW_MS_SQL});
100
+ END;
101
+ `;
102
+ await this.db.exec(insert('insert', 'NEW', 'NEW.data'));
103
+ await this.db.exec(insert('update', 'NEW', 'NEW.data'));
104
+ await this.db.exec(insert('delete', 'OLD', 'NULL'));
105
+ this.installedTriggers.add(collection);
106
+ }
107
+ /**
108
+ * Removes the capture triggers for a collection. Changes to it stop being
109
+ * recorded — existing queued rows are untouched.
110
+ */
111
+ async removeTriggers(collection) {
112
+ for (const op of ['insert', 'update', 'delete']) {
113
+ await this.db.exec(`DROP TRIGGER IF EXISTS ${quoteIdent(triggerName(collection, op))}`);
114
+ }
115
+ this.installedTriggers.delete(collection);
116
+ }
117
+ /** Lists user collections, excluding MongoLite's own bookkeeping tables. */
118
+ async listCollections() {
119
+ const rows = await this.db.all(`SELECT name FROM sqlite_master
120
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__mongolite%'
121
+ ORDER BY name`);
122
+ return rows.map((row) => row.name);
123
+ }
124
+ /**
125
+ * Reconciles the in-memory "already installed" cache with what is actually in
126
+ * the schema.
127
+ *
128
+ * Dropping a table drops its triggers with it, so a collection that was
129
+ * dropped and recreated would otherwise be silently uncaptured — the cache
130
+ * would insist its triggers were still there.
131
+ */
132
+ async refreshTriggerCache() {
133
+ const rows = await this.db.all(`SELECT name FROM sqlite_master WHERE type = 'trigger' AND name LIKE '${SYNC_OBJECT_PREFIX}\\_%' ESCAPE '\\'`);
134
+ const present = new Set(rows.map((row) => row.name));
135
+ for (const collection of [...this.installedTriggers]) {
136
+ const complete = ['insert', 'update', 'delete'].every((op) => present.has(triggerName(collection, op)));
137
+ if (!complete)
138
+ this.installedTriggers.delete(collection);
139
+ }
140
+ }
141
+ /** The newest outbox id, or 0 when the log is empty. */
142
+ async currentSequence() {
143
+ const row = await this.db.get(`SELECT MAX(id) AS seq FROM ${OUTBOX_TABLE}`);
144
+ return row?.seq ?? 0;
145
+ }
146
+ /** Reads the persisted checkpoint for a replicator, creating its state row if absent. */
147
+ async loadState(name) {
148
+ await this.ensureSchema();
149
+ const row = await this.db.get(`SELECT checkpoint, backfilled FROM ${STATE_TABLE} WHERE name = ?`, [name]);
150
+ if (row) {
151
+ let backfilled = [];
152
+ try {
153
+ const parsed = JSON.parse(row.backfilled);
154
+ if (Array.isArray(parsed))
155
+ backfilled = parsed.filter((v) => typeof v === 'string');
156
+ }
157
+ catch {
158
+ // Corrupt bookkeeping should not wedge replication; treat as "nothing backfilled".
159
+ }
160
+ return { checkpoint: Number(row.checkpoint) || 0, backfilled };
161
+ }
162
+ // A brand new replicator starts from the current tail. Anything already in
163
+ // the collections is handled by the backfill, not by replaying the log.
164
+ const checkpoint = await this.currentSequence();
165
+ await this.db.run(`INSERT INTO ${STATE_TABLE} (name, checkpoint, backfilled, updated_at)
166
+ VALUES (?, ?, '[]', ${NOW_MS_SQL})`, [name, checkpoint]);
167
+ return { checkpoint, backfilled: [] };
168
+ }
169
+ /**
170
+ * Advances a replicator's checkpoint.
171
+ *
172
+ * This is the acknowledgement, and it is a single atomic statement: a crash
173
+ * either leaves the old checkpoint (the batch is replayed, which is safe
174
+ * because upstream writes are idempotent) or the new one.
175
+ */
176
+ async saveCheckpoint(name, checkpoint) {
177
+ await this.db.run(`UPDATE ${STATE_TABLE} SET checkpoint = ?, updated_at = ${NOW_MS_SQL} WHERE name = ?`, [checkpoint, name]);
178
+ }
179
+ /** Records that a collection's existing contents have been backfilled. */
180
+ async markBackfilled(name, collections) {
181
+ await this.db.run(`UPDATE ${STATE_TABLE} SET backfilled = ?, updated_at = ${NOW_MS_SQL} WHERE name = ?`, [JSON.stringify(collections), name]);
182
+ }
183
+ /** Forgets a replicator, releasing the outbox rows its checkpoint was pinning. */
184
+ async unregister(name) {
185
+ await this.ensureSchema();
186
+ await this.db.run(`DELETE FROM ${STATE_TABLE} WHERE name = ?`, [name]);
187
+ }
188
+ /** Reads the next batch of changes after `checkpoint`, oldest first. */
189
+ async readBatch(checkpoint, limit, collections) {
190
+ const params = [checkpoint];
191
+ let filter = '';
192
+ if (collections) {
193
+ if (collections.length === 0)
194
+ return [];
195
+ filter = ` AND collection_name IN (${collections.map(() => '?').join(', ')})`;
196
+ params.push(...collections);
197
+ }
198
+ params.push(limit);
199
+ const rows = await this.db.all(`SELECT id, collection_name, document_id, operation, full_document, captured_at
200
+ FROM ${OUTBOX_TABLE}
201
+ WHERE id > ?${filter}
202
+ ORDER BY id ASC
203
+ LIMIT ?`, params);
204
+ return rows.map((row) => ({
205
+ id: Number(row.id),
206
+ collection: row.collection_name,
207
+ documentId: row.document_id,
208
+ operation: row.operation,
209
+ document: parseDocument(row.full_document, row.document_id),
210
+ capturedAt: Number(row.captured_at),
211
+ }));
212
+ }
213
+ /** Count of rows a replicator still has to apply. */
214
+ async pendingCount(checkpoint, collections) {
215
+ const params = [checkpoint];
216
+ let filter = '';
217
+ if (collections) {
218
+ if (collections.length === 0)
219
+ return 0;
220
+ filter = ` AND collection_name IN (${collections.map(() => '?').join(', ')})`;
221
+ params.push(...collections);
222
+ }
223
+ const row = await this.db.get(`SELECT COUNT(*) AS total FROM ${OUTBOX_TABLE} WHERE id > ?${filter}`, params);
224
+ return Number(row?.total ?? 0);
225
+ }
226
+ /**
227
+ * Deletes rows every registered replicator has already applied.
228
+ * @returns Number of rows removed.
229
+ */
230
+ async prune() {
231
+ const row = await this.db.get(`SELECT MIN(checkpoint) AS floor FROM ${STATE_TABLE}`);
232
+ const floor = row?.floor;
233
+ if (floor === null || floor === undefined)
234
+ return 0;
235
+ const result = await this.db.run(`DELETE FROM ${OUTBOX_TABLE} WHERE id <= ?`, [floor]);
236
+ return result.changes;
237
+ }
238
+ /**
239
+ * Drops superseded revisions, keeping only the newest row per document.
240
+ *
241
+ * Safe because replication carries whole documents rather than deltas: the
242
+ * newest row alone reproduces the same upstream state. Rows at or below the
243
+ * slowest replicator's checkpoint are left to {@link SyncOutbox.prune}.
244
+ *
245
+ * @returns Number of rows removed.
246
+ */
247
+ async compact() {
248
+ const result = await this.db.run(`DELETE FROM ${OUTBOX_TABLE}
249
+ WHERE id NOT IN (
250
+ SELECT MAX(id) FROM ${OUTBOX_TABLE} GROUP BY collection_name, document_id
251
+ )
252
+ AND id > COALESCE((SELECT MIN(checkpoint) FROM ${STATE_TABLE}), 0)`);
253
+ return result.changes;
254
+ }
255
+ /**
256
+ * Appends a change back onto the log so it is retried on a later drain.
257
+ *
258
+ * Used when a conflict is resolved in favour of the local version: the shadow has
259
+ * since been refreshed, so the replayed push diffs against current upstream state and
260
+ * carries the version it actually lost to.
261
+ */
262
+ async requeue(collection, documentId, document) {
263
+ await this.ensureSchema();
264
+ // `_id` lives in its own column locally and is re-attached on read, so it is
265
+ // stripped here to match what the capture triggers store.
266
+ let payload = null;
267
+ if (document) {
268
+ const rest = { ...document };
269
+ delete rest._id;
270
+ payload = JSON.stringify(rest);
271
+ }
272
+ await this.db.run(`INSERT INTO ${OUTBOX_TABLE}
273
+ (collection_name, document_id, operation, full_document, captured_at)
274
+ VALUES (?, ?, ?, ?, ${NOW_MS_SQL})`, [collection, documentId, document ? 'update' : 'delete', payload]);
275
+ }
276
+ /** Moves a permanently-rejected change out of the stream so it cannot stall replication. */
277
+ async deadLetter(replicator, record, error) {
278
+ await this.db.run(`INSERT INTO ${DEAD_LETTER_TABLE}
279
+ (replicator, collection_name, document_id, operation, full_document, error, failed_at)
280
+ VALUES (?, ?, ?, ?, ?, ?, ${NOW_MS_SQL})`, [
281
+ replicator,
282
+ record.collection,
283
+ record.documentId,
284
+ record.operation,
285
+ record.document === null || record.document === undefined
286
+ ? null
287
+ : JSON.stringify(record.document),
288
+ error,
289
+ ]);
290
+ }
291
+ /** Reads dead-lettered changes, newest first. */
292
+ async listDeadLetters(replicator, limit = 100) {
293
+ await this.ensureSchema();
294
+ const where = replicator ? 'WHERE replicator = ?' : '';
295
+ const params = replicator ? [replicator, limit] : [limit];
296
+ const rows = await this.db.all(`SELECT id, collection_name, document_id, operation, full_document, error, failed_at
297
+ FROM ${DEAD_LETTER_TABLE} ${where} ORDER BY id DESC LIMIT ?`, params);
298
+ return rows.map((row) => ({
299
+ id: Number(row.id),
300
+ collection: row.collection_name,
301
+ documentId: row.document_id,
302
+ operation: row.operation,
303
+ document: parseDocument(row.full_document, row.document_id),
304
+ error: row.error,
305
+ failedAt: Number(row.failed_at),
306
+ }));
307
+ }
308
+ /** Removes dead-lettered rows, e.g. after replaying them by hand. */
309
+ async clearDeadLetters(replicator) {
310
+ await this.ensureSchema();
311
+ const result = replicator
312
+ ? await this.db.run(`DELETE FROM ${DEAD_LETTER_TABLE} WHERE replicator = ?`, [replicator])
313
+ : await this.db.run(`DELETE FROM ${DEAD_LETTER_TABLE}`);
314
+ return result.changes;
315
+ }
316
+ /**
317
+ * Streams a collection's current contents in `_id` order, for the initial
318
+ * backfill. Keyset pagination keeps memory flat on large collections.
319
+ */
320
+ async *scanCollection(collection, chunkSize) {
321
+ // `null` on the first pass so an empty-string `_id` is not skipped.
322
+ let after = null;
323
+ for (;;) {
324
+ const rows = await this.db.all(`SELECT _id, data FROM ${quoteIdent(collection)}
325
+ ${after === null ? '' : 'WHERE _id > ?'}
326
+ ORDER BY _id ASC LIMIT ?`, after === null ? [chunkSize] : [after, chunkSize]);
327
+ if (rows.length === 0)
328
+ return;
329
+ yield rows.map((row) => ({
330
+ documentId: row._id,
331
+ document: parseDocument(row.data, row._id),
332
+ }));
333
+ after = rows[rows.length - 1]._id;
334
+ }
335
+ }
336
+ }
337
+ /**
338
+ * Parses a stored document, re-attaching the `_id` that lives in its own column.
339
+ * Returns `null` for deletes and for rows whose JSON is unreadable — the caller
340
+ * dead-letters the latter rather than crashing the replicator.
341
+ */
342
+ function parseDocument(json, documentId) {
343
+ if (json === null || json === undefined)
344
+ return null;
345
+ try {
346
+ const parsed = JSON.parse(json);
347
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
348
+ return null;
349
+ return { _id: documentId, ...parsed };
350
+ }
351
+ catch {
352
+ return null;
353
+ }
354
+ }
355
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../src/sync/outbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import type { MongoWriteCommand } from './commands.js';
2
+ import type { SyncApplyConflict, SyncApplyFailure, SyncOperation } from './types.js';
3
+ /**
4
+ * Protocol version, sent with every request.
5
+ *
6
+ * A receiver rejects a version it does not understand rather than guessing, so an
7
+ * upgraded client talking to an old server fails loudly instead of silently writing
8
+ * something unintended.
9
+ */
10
+ export declare const SYNC_PROTOCOL_VERSION = 1;
11
+ /** Content type used for request and response bodies. */
12
+ export declare const SYNC_CONTENT_TYPE = "application/json";
13
+ /** One operation as it crosses the wire. */
14
+ export interface WireOperation {
15
+ /** Upstream collection name, after any renaming. */
16
+ collection: string;
17
+ documentId: string;
18
+ type: 'upsert' | 'delete';
19
+ /** Upstream `_v` this operation asserts against; `null` when the document is new. */
20
+ baseVersion?: number | null;
21
+ /** Field-level change, when a shadow existed to diff against. */
22
+ diff?: {
23
+ set: Record<string, unknown>;
24
+ unset: string[];
25
+ };
26
+ /** Full document, for creates and for the unversioned mode. */
27
+ document?: Record<string, unknown>;
28
+ /**
29
+ * The MongoDB command this operation resolves to.
30
+ *
31
+ * Sent so a forwarding API can inspect or pass it straight through. The receiver
32
+ * **rebuilds it** from the fields above rather than executing it, unless explicitly
33
+ * configured to trust the client — see `createSyncReceiver`.
34
+ */
35
+ command?: MongoWriteCommand;
36
+ }
37
+ /** Body of a write request. */
38
+ export interface SyncApplyRequest {
39
+ protocol: number;
40
+ /** Replicator name, for server-side logging and rate limiting. */
41
+ replicator?: string;
42
+ operations: WireOperation[];
43
+ }
44
+ /** Body of a write response — mirrors `SyncApplyResult`. */
45
+ export interface SyncApplyResponse {
46
+ protocol: number;
47
+ applied: number;
48
+ failures?: SyncApplyFailure[];
49
+ conflicts?: SyncApplyConflict[];
50
+ }
51
+ /** Body of a read-back request, used to refresh a shadow after a conflict. */
52
+ export interface SyncFetchRequest {
53
+ protocol: number;
54
+ collection: string;
55
+ documentIds: string[];
56
+ }
57
+ /** Body of a read-back response. */
58
+ export interface SyncFetchResponse {
59
+ protocol: number;
60
+ documents: Record<string, unknown>[];
61
+ }
62
+ /** Raised when a payload is malformed or speaks a version we do not implement. */
63
+ export declare class SyncProtocolError extends Error {
64
+ /** Suggested HTTP status for a receiver to return. */
65
+ readonly status: number;
66
+ constructor(message: string,
67
+ /** Suggested HTTP status for a receiver to return. */
68
+ status?: number);
69
+ }
70
+ /** Serialises a body to relaxed Extended JSON. */
71
+ export declare function encodeBody(value: unknown): string;
72
+ /** Parses a relaxed Extended JSON body back into BSON-typed values. */
73
+ export declare function decodeBody<T>(raw: string | Record<string, unknown>): T;
74
+ /** Converts an internal operation into its wire form. */
75
+ export declare function toWireOperation(op: SyncOperation, command?: MongoWriteCommand): WireOperation;
76
+ /**
77
+ * Validates a wire operation and converts it back into an internal one.
78
+ *
79
+ * Everything reaching a receiver is untrusted input, so this checks shape rather than
80
+ * assuming it. `index` and `sourceCollection` are reconstructed locally — a client does
81
+ * not get to choose them.
82
+ */
83
+ export declare function fromWireOperation(value: unknown, index: number): SyncOperation;
84
+ /** Validates the envelope of an apply request and returns its operations. */
85
+ export declare function parseApplyRequest(body: unknown): {
86
+ replicator: string;
87
+ operations: SyncOperation[];
88
+ };
89
+ /** Validates the envelope of a fetch request. */
90
+ export declare function parseFetchRequest(body: unknown): {
91
+ collection: string;
92
+ documentIds: string[];
93
+ };