@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.
- package/README.md +79 -21
- package/dist/cjs/collection.js +8 -1
- package/dist/cjs/index.js +108 -1
- package/dist/cjs/mongo-client.js +18 -1
- package/dist/cjs/server.js +164 -0
- package/dist/cjs/sync/commands.js +126 -0
- package/dist/cjs/sync/diff.js +174 -0
- package/dist/cjs/sync/httpSink.js +159 -0
- package/dist/cjs/sync/index.js +40 -0
- package/dist/cjs/sync/mongoExecutor.js +306 -0
- package/dist/cjs/sync/mongoSink.js +161 -0
- package/dist/cjs/sync/outbox.js +358 -0
- package/dist/cjs/sync/protocol.js +177 -0
- package/dist/cjs/sync/replicator.js +747 -0
- package/dist/cjs/sync/shadow.js +178 -0
- package/dist/cjs/sync/types.js +2 -0
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js.map +1 -1
- package/dist/collection.js +8 -1
- package/dist/collection.js.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/dist/mongo-client.d.ts +13 -0
- package/dist/mongo-client.js +18 -1
- package/dist/mongo-client.js.map +1 -1
- package/dist/server.d.ts +97 -0
- package/dist/server.js +158 -0
- package/dist/server.js.map +1 -0
- package/dist/sync/commands.d.ts +49 -0
- package/dist/sync/commands.js +118 -0
- package/dist/sync/commands.js.map +1 -0
- package/dist/sync/diff.d.ts +55 -0
- package/dist/sync/diff.js +169 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/httpSink.d.ts +93 -0
- package/dist/sync/httpSink.js +155 -0
- package/dist/sync/httpSink.js.map +1 -0
- package/dist/sync/index.d.ts +17 -0
- package/dist/sync/index.js +10 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/mongoExecutor.d.ts +37 -0
- package/dist/sync/mongoExecutor.js +302 -0
- package/dist/sync/mongoExecutor.js.map +1 -0
- package/dist/sync/mongoSink.d.ts +109 -0
- package/dist/sync/mongoSink.js +123 -0
- package/dist/sync/mongoSink.js.map +1 -0
- package/dist/sync/outbox.d.ts +120 -0
- package/dist/sync/outbox.js +355 -0
- package/dist/sync/outbox.js.map +1 -0
- package/dist/sync/protocol.d.ts +93 -0
- package/dist/sync/protocol.js +168 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/replicator.d.ts +177 -0
- package/dist/sync/replicator.js +744 -0
- package/dist/sync/replicator.js.map +1 -0
- package/dist/sync/shadow.d.ts +54 -0
- package/dist/sync/shadow.js +173 -0
- package/dist/sync/shadow.js.map +1 -0
- package/dist/sync/types.d.ts +388 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- 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
|
+
};
|