@sqlite-sync/core 0.9.4 → 0.10.1
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/admit-client-events-BDrJw4SK.js +35 -0
- package/dist/admit-client-events-BDrJw4SK.js.map +1 -0
- package/dist/crdt-sync-remote-source-CugbiosG.js +508 -0
- package/dist/crdt-sync-remote-source-CugbiosG.js.map +1 -0
- package/dist/{utils-Dr5JwcTP.js → crdt-table-schema-2w_AIpsV.js} +9 -2
- package/dist/crdt-table-schema-2w_AIpsV.js.map +1 -0
- package/dist/{index-Dbbd4flq.d.ts → index-D0jdmcYF.d.ts} +36 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -50
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +3 -3
- package/dist/{make-crdt-table-CvpTHmRY.js → make-crdt-table-D8VQ-ptj.js} +23 -14
- package/dist/make-crdt-table-D8VQ-ptj.js.map +1 -0
- package/dist/{reset-state-4Hn95CKG.js → reset-state-DNPkxGim.js} +39 -429
- package/dist/reset-state-DNPkxGim.js.map +1 -0
- package/dist/server.d.ts +30 -2
- package/dist/server.js +103 -3
- package/dist/server.js.map +1 -1
- package/dist/{system-schema-CkglcfTS.js → system-schema-C2JZDv6L.js} +3 -10
- package/dist/system-schema-C2JZDv6L.js.map +1 -0
- package/dist/worker.d.ts +3 -2
- package/dist/worker.js +59 -10
- package/dist/worker.js.map +1 -1
- package/package.json +2 -1
- package/dist/make-crdt-table-CvpTHmRY.js.map +0 -1
- package/dist/reset-state-4Hn95CKG.js.map +0 -1
- package/dist/system-schema-CkglcfTS.js.map +0 -1
- package/dist/utils-Dr5JwcTP.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/schema/admit-client-events.ts
|
|
2
|
+
function buildWriteOriginByName(schema) {
|
|
3
|
+
const writeOriginByName = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const { crdtTableName, baseTableName } of schema.tablesConfig) {
|
|
5
|
+
const writeOrigin = schema.tables[crdtTableName]?.writeOrigin ?? "any";
|
|
6
|
+
writeOriginByName.set(crdtTableName, writeOrigin);
|
|
7
|
+
writeOriginByName.set(baseTableName, writeOrigin);
|
|
8
|
+
}
|
|
9
|
+
return writeOriginByName;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Splits a client push into events the hub should persist and events it must drop. Only
|
|
13
|
+
* datasets that exactly match the base table name of a declared table without
|
|
14
|
+
* `{ writes: "server" }` are admitted; server-only tables, crdt view names, and undeclared
|
|
15
|
+
* datasets are skipped.
|
|
16
|
+
*/
|
|
17
|
+
function admitClientEvents(opts) {
|
|
18
|
+
const admitted = [];
|
|
19
|
+
const skipped = [];
|
|
20
|
+
for (const event of opts.events) if (getClientDatasetRejection(opts.syncDbSchema, event.dataset)) skipped.push(event);
|
|
21
|
+
else admitted.push(event);
|
|
22
|
+
return {
|
|
23
|
+
admitted,
|
|
24
|
+
skipped
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function getClientDatasetRejection(schema, dataset) {
|
|
28
|
+
const config = schema.tablesConfig.find((tableConfig) => tableConfig.baseTableName === dataset);
|
|
29
|
+
if (!config) return "undeclared-dataset";
|
|
30
|
+
return schema.tables[config.crdtTableName]?.writeOrigin === "server" ? "server-only-dataset" : null;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { buildWriteOriginByName as n, getClientDatasetRejection as r, admitClientEvents as t };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=admit-client-events-BDrJw4SK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admit-client-events-BDrJw4SK.js","names":[],"sources":["../src/schema/admit-client-events.ts"],"sourcesContent":["import type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport type { WriteOrigin } from \"./table-builder\";\n\nexport type AdmitClientEventsResult<T> = {\n admitted: T[];\n skipped: T[];\n};\n\ntype SchemaForAdmitClientEvents = Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">;\n\nexport type ClientDatasetRejectionReason = \"server-only-dataset\" | \"undeclared-dataset\";\n\nexport function buildWriteOriginByName(\n schema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">,\n): Map<string, WriteOrigin> {\n const writeOriginByName = new Map<string, WriteOrigin>();\n for (const { crdtTableName, baseTableName } of schema.tablesConfig) {\n const writeOrigin = schema.tables[crdtTableName]?.writeOrigin ?? \"any\";\n writeOriginByName.set(crdtTableName, writeOrigin);\n writeOriginByName.set(baseTableName, writeOrigin);\n }\n return writeOriginByName;\n}\n\n/**\n * Splits a client push into events the hub should persist and events it must drop. Only\n * datasets that exactly match the base table name of a declared table without\n * `{ writes: \"server\" }` are admitted; server-only tables, crdt view names, and undeclared\n * datasets are skipped.\n */\nexport function admitClientEvents<T extends { dataset: string }>(opts: {\n syncDbSchema: SchemaForAdmitClientEvents;\n events: readonly T[];\n}): AdmitClientEventsResult<T> {\n const admitted: T[] = [];\n const skipped: T[] = [];\n for (const event of opts.events) {\n if (getClientDatasetRejection(opts.syncDbSchema, event.dataset)) {\n skipped.push(event);\n } else {\n admitted.push(event);\n }\n }\n return { admitted, skipped };\n}\n\nexport function getClientDatasetRejection(\n schema: SchemaForAdmitClientEvents,\n dataset: string,\n): ClientDatasetRejectionReason | null {\n const config = schema.tablesConfig.find((tableConfig) => tableConfig.baseTableName === dataset);\n if (!config) {\n return \"undeclared-dataset\";\n }\n return schema.tables[config.crdtTableName]?.writeOrigin === \"server\" ? \"server-only-dataset\" : null;\n}\n"],"mappings":";AAYA,SAAgB,uBACd,QAC0B;CAC1B,MAAM,oCAAoB,IAAI,IAAyB;CACvD,KAAK,MAAM,EAAE,eAAe,mBAAmB,OAAO,cAAc;EAClE,MAAM,cAAc,OAAO,OAAO,cAAc,EAAE,eAAe;EACjE,kBAAkB,IAAI,eAAe,WAAW;EAChD,kBAAkB,IAAI,eAAe,WAAW;CAClD;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAiD,MAGlC;CAC7B,MAAM,WAAgB,CAAC;CACvB,MAAM,UAAe,CAAC;CACtB,KAAK,MAAM,SAAS,KAAK,QACvB,IAAI,0BAA0B,KAAK,cAAc,MAAM,OAAO,GAC5D,QAAQ,KAAK,KAAK;MAElB,SAAS,KAAK,KAAK;CAGvB,OAAO;EAAE;EAAU;CAAQ;AAC7B;AAEA,SAAgB,0BACd,QACA,SACqC;CACrC,MAAM,SAAS,OAAO,aAAa,MAAM,gBAAgB,YAAY,kBAAkB,OAAO;CAC9F,IAAI,CAAC,QACH,OAAO;CAET,OAAO,OAAO,OAAO,OAAO,cAAc,EAAE,gBAAgB,WAAW,wBAAwB;AACjG"}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { m as tryCatchAsync, o as createTypedEventTarget, s as ensureSingletonExecution } from "./crdt-table-schema-2w_AIpsV.js";
|
|
2
|
+
//#region src/hlc.ts
|
|
3
|
+
const MAX_COUNTER = 36 ** 5 - 1;
|
|
4
|
+
const DEFAULT_MAX_DRIFT_MS = 216e5;
|
|
5
|
+
var HLCCounter = class {
|
|
6
|
+
timestamp;
|
|
7
|
+
counter;
|
|
8
|
+
nodeId;
|
|
9
|
+
getTimestamp;
|
|
10
|
+
maxDrift;
|
|
11
|
+
constructor(nodeId, getTimestamp, maxDrift = DEFAULT_MAX_DRIFT_MS) {
|
|
12
|
+
this.timestamp = getTimestamp();
|
|
13
|
+
this.counter = 0;
|
|
14
|
+
this.nodeId = nodeId;
|
|
15
|
+
this.getTimestamp = getTimestamp;
|
|
16
|
+
this.maxDrift = maxDrift;
|
|
17
|
+
}
|
|
18
|
+
getCurrentHLC() {
|
|
19
|
+
return {
|
|
20
|
+
timestamp: this.timestamp,
|
|
21
|
+
counter: this.counter,
|
|
22
|
+
nodeId: this.nodeId
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
getNextHLC() {
|
|
26
|
+
const now = this.getTimestamp();
|
|
27
|
+
if (now > this.timestamp) {
|
|
28
|
+
this.timestamp = now;
|
|
29
|
+
this.counter = 0;
|
|
30
|
+
return this.getCurrentHLC();
|
|
31
|
+
}
|
|
32
|
+
this.counter++;
|
|
33
|
+
this.rollOverCounter();
|
|
34
|
+
return this.getCurrentHLC();
|
|
35
|
+
}
|
|
36
|
+
/** Restore already-persisted causal state, regardless of wall-clock drift. */
|
|
37
|
+
restoreHLC(hlc) {
|
|
38
|
+
if (hlc.timestamp > this.timestamp) {
|
|
39
|
+
this.timestamp = hlc.timestamp;
|
|
40
|
+
this.counter = hlc.counter;
|
|
41
|
+
} else if (hlc.timestamp === this.timestamp) this.counter = Math.max(this.counter, hlc.counter);
|
|
42
|
+
}
|
|
43
|
+
mergeHLC(hlc) {
|
|
44
|
+
const now = this.getTimestamp();
|
|
45
|
+
if (hlc.timestamp - now > this.maxDrift) {
|
|
46
|
+
console.warn(`HLC: ignoring far-future timestamp (remote=${hlc.timestamp}, local=${now}, drift=${hlc.timestamp - now}ms)`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (this.timestamp === hlc.timestamp) this.counter = Math.max(this.counter, hlc.counter) + 1;
|
|
50
|
+
else if (this.timestamp > hlc.timestamp) this.counter++;
|
|
51
|
+
else {
|
|
52
|
+
this.timestamp = hlc.timestamp;
|
|
53
|
+
this.counter = hlc.counter + 1;
|
|
54
|
+
}
|
|
55
|
+
this.rollOverCounter();
|
|
56
|
+
}
|
|
57
|
+
rollOverCounter() {
|
|
58
|
+
if (this.counter > MAX_COUNTER) {
|
|
59
|
+
this.timestamp += 1;
|
|
60
|
+
this.counter = 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function serializeHLC(hlc) {
|
|
65
|
+
return `${hlc.timestamp.toString().padStart(15, "0")}:${hlc.counter.toString(36).padStart(5, "0")}:${hlc.nodeId}`;
|
|
66
|
+
}
|
|
67
|
+
function deserializeHLC(serialized) {
|
|
68
|
+
const parts = serialized.split(":");
|
|
69
|
+
if (parts.length < 3) throw new Error(`Invalid HLC format: expected at least 3 colon-separated segments, got ${parts.length}`);
|
|
70
|
+
const timestamp = parseInt(parts[0], 10);
|
|
71
|
+
const counter = parseInt(parts[1], 36);
|
|
72
|
+
if (Number.isNaN(timestamp) || Number.isNaN(counter)) throw new Error(`Invalid HLC values: timestamp=${parts[0]}, counter=${parts[1]}`);
|
|
73
|
+
return {
|
|
74
|
+
timestamp,
|
|
75
|
+
counter,
|
|
76
|
+
nodeId: parts.slice(2).join(":")
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function compareHLC(one, two) {
|
|
80
|
+
if (one.timestamp === two.timestamp) {
|
|
81
|
+
if (one.counter === two.counter) {
|
|
82
|
+
if (one.nodeId === two.nodeId) return 0;
|
|
83
|
+
return one.nodeId < two.nodeId ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
return one.counter - two.counter;
|
|
86
|
+
}
|
|
87
|
+
return one.timestamp - two.timestamp;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/schema/validate-crdt-event.ts
|
|
91
|
+
var CrdtEventValidationError = class extends Error {
|
|
92
|
+
errors;
|
|
93
|
+
constructor(errors) {
|
|
94
|
+
super(`Invalid CRDT events: ${errors.join("; ")}`);
|
|
95
|
+
this.errors = errors;
|
|
96
|
+
this.name = "CrdtEventValidationError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const crdtEventTypes = [
|
|
100
|
+
"item-created",
|
|
101
|
+
"item-updated",
|
|
102
|
+
"item-deleted"
|
|
103
|
+
];
|
|
104
|
+
function validateNewCrdtEvent(schema, input) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
if (!isRecord(input)) return {
|
|
107
|
+
success: false,
|
|
108
|
+
errors: ["Event must be an object"]
|
|
109
|
+
};
|
|
110
|
+
const type = input.type;
|
|
111
|
+
const dataset = input.dataset;
|
|
112
|
+
const itemId = input.item_id;
|
|
113
|
+
let payload = input.payload;
|
|
114
|
+
let payloadParseFailed = false;
|
|
115
|
+
if (typeof payload === "string") try {
|
|
116
|
+
payload = JSON.parse(payload);
|
|
117
|
+
} catch {
|
|
118
|
+
payloadParseFailed = true;
|
|
119
|
+
errors.push(`Event payload is not valid JSON`);
|
|
120
|
+
}
|
|
121
|
+
if (!isCrdtEventType(type)) errors.push(`Invalid event type "${String(type)}"`);
|
|
122
|
+
if (typeof dataset !== "string") errors.push(`Event dataset must be a string`);
|
|
123
|
+
if (typeof itemId !== "string") errors.push(`Event item_id must be a string`);
|
|
124
|
+
if (!payloadParseFailed && !isRecord(payload)) errors.push(`Event payload must be an object`);
|
|
125
|
+
const datasetMatch = typeof dataset === "string" ? findTableForDataset(schema, dataset) : null;
|
|
126
|
+
if (typeof dataset === "string" && !datasetMatch) errors.push(`Unknown dataset "${dataset}"`);
|
|
127
|
+
if (isCrdtEventType(type) && datasetMatch && isRecord(payload)) switch (type) {
|
|
128
|
+
case "item-created": {
|
|
129
|
+
const result = datasetMatch.table.validatePayload(payload, { event: "item-created" });
|
|
130
|
+
if (!result.success) errors.push(...result.errors.map((error) => `payload: ${error}`));
|
|
131
|
+
if (typeof itemId === "string" && "id" in payload && payload.id !== itemId) errors.push(`payload: Column "id" must match item_id "${itemId}"`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "item-updated": {
|
|
135
|
+
const result = datasetMatch.table.validatePayload(payload, { event: "item-updated" });
|
|
136
|
+
if (!result.success) errors.push(...result.errors.map((error) => `payload: ${error}`));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "item-deleted": if (Object.keys(payload).length > 0) errors.push(`payload: item-deleted events must have an empty payload`);
|
|
140
|
+
}
|
|
141
|
+
if (errors.length > 0) return {
|
|
142
|
+
success: false,
|
|
143
|
+
errors
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
event: {
|
|
148
|
+
type,
|
|
149
|
+
dataset: datasetMatch?.baseTableName ?? dataset,
|
|
150
|
+
item_id: itemId,
|
|
151
|
+
payload
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function findTableForDataset(schema, dataset) {
|
|
156
|
+
const config = schema.tablesConfig.find((tableConfig) => tableConfig.baseTableName === dataset || tableConfig.crdtTableName === dataset);
|
|
157
|
+
if (config) {
|
|
158
|
+
const table = schema.tables[config.crdtTableName];
|
|
159
|
+
return table ? {
|
|
160
|
+
table,
|
|
161
|
+
baseTableName: config.baseTableName
|
|
162
|
+
} : null;
|
|
163
|
+
}
|
|
164
|
+
const table = Object.hasOwn(schema.tables, dataset) ? schema.tables[dataset] : void 0;
|
|
165
|
+
return table ? {
|
|
166
|
+
table,
|
|
167
|
+
baseTableName: dataset
|
|
168
|
+
} : null;
|
|
169
|
+
}
|
|
170
|
+
function isRecord(value) {
|
|
171
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
172
|
+
}
|
|
173
|
+
function isCrdtEventType(value) {
|
|
174
|
+
return typeof value === "string" && crdtEventTypes.includes(value);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/sqlite-crdt/retry-remote-operation.ts
|
|
178
|
+
const REMOTE_RETRY_OPTIONS = {
|
|
179
|
+
maxAttempts: 3,
|
|
180
|
+
backoffBaseMs: 100,
|
|
181
|
+
backoffExponent: 1.5,
|
|
182
|
+
backoffJitterMs: 150,
|
|
183
|
+
timeoutMs: 1e4
|
|
184
|
+
};
|
|
185
|
+
var RetryTimeoutError = class extends Error {
|
|
186
|
+
previous;
|
|
187
|
+
constructor(message, previous) {
|
|
188
|
+
super(message);
|
|
189
|
+
this.previous = previous;
|
|
190
|
+
this.name = "TimeoutError";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const applyJitter = (delayMs, maxJitterMs) => {
|
|
194
|
+
const jitter = Math.random() * maxJitterMs * (Math.random() > .5 ? 1 : -1);
|
|
195
|
+
return Math.max(0, delayMs + jitter);
|
|
196
|
+
};
|
|
197
|
+
const delay = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
198
|
+
const withTimeout = async (operation, timeoutMs, previousError) => {
|
|
199
|
+
let timeoutId;
|
|
200
|
+
try {
|
|
201
|
+
return await Promise.race([operation(), new Promise((_, reject) => {
|
|
202
|
+
timeoutId = setTimeout(() => reject(new RetryTimeoutError("Remote operation timed out", previousError)), timeoutMs);
|
|
203
|
+
})]);
|
|
204
|
+
} finally {
|
|
205
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const retryRemoteOperation = async (operation, options) => {
|
|
209
|
+
let lastError;
|
|
210
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) try {
|
|
211
|
+
return await withTimeout(operation, options.timeoutMs, lastError);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
lastError = error;
|
|
214
|
+
if (attempt >= options.maxAttempts || options.shouldRetry?.(error) === false) throw error;
|
|
215
|
+
const backoffDelay = applyJitter(options.backoffBaseMs * options.backoffExponent ** (attempt - 1), options.backoffJitterMs);
|
|
216
|
+
if (backoffDelay > 0) await delay(backoffDelay);
|
|
217
|
+
}
|
|
218
|
+
throw lastError;
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/sqlite-crdt/crdt-sync-remote-source.ts
|
|
222
|
+
const DISCONNECT_WATCHDOG_MS = 15e3;
|
|
223
|
+
var SchemaVersionMismatchError = class extends Error {
|
|
224
|
+
remoteSchemaVersion;
|
|
225
|
+
localSchemaVersion;
|
|
226
|
+
constructor(remoteSchemaVersion, localSchemaVersion) {
|
|
227
|
+
super(`Schema version mismatch: remote ${remoteSchemaVersion} != local ${localSchemaVersion}`);
|
|
228
|
+
this.remoteSchemaVersion = remoteSchemaVersion;
|
|
229
|
+
this.localSchemaVersion = localSchemaVersion;
|
|
230
|
+
this.name = "SchemaVersionMismatchError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const createCrdtSyncRemoteSource = ({ bufferSize, storage, migrator, pullSyncId, pushSyncId, nodeId, remoteFactory }) => {
|
|
234
|
+
const eventTarget = createTypedEventTarget();
|
|
235
|
+
let remoteState = {
|
|
236
|
+
type: "offline",
|
|
237
|
+
reason: "NOT_INITIALIZED",
|
|
238
|
+
deSynced: false,
|
|
239
|
+
schemaVersionMismatched: false
|
|
240
|
+
};
|
|
241
|
+
const patchRemoteState = (state) => {
|
|
242
|
+
remoteState = {
|
|
243
|
+
...remoteState,
|
|
244
|
+
...state
|
|
245
|
+
};
|
|
246
|
+
eventTarget.dispatchEvent("state-changed", remoteState.type);
|
|
247
|
+
};
|
|
248
|
+
let current = null;
|
|
249
|
+
let disposed = false;
|
|
250
|
+
let wantsOnline = false;
|
|
251
|
+
const disconnectSource = (source) => {
|
|
252
|
+
const watchdog = setTimeout(() => {
|
|
253
|
+
console.error(`Remote source did not disconnect within ${DISCONNECT_WATCHDOG_MS}ms`);
|
|
254
|
+
}, DISCONNECT_WATCHDOG_MS);
|
|
255
|
+
tryCatchAsync(async () => await source.disconnect?.()).then((result) => {
|
|
256
|
+
clearTimeout(watchdog);
|
|
257
|
+
if (!result.success) console.warn("Error while disconnecting from remote source", result.error);
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const closeConnection = (connection, reason) => {
|
|
261
|
+
if (current !== connection) return;
|
|
262
|
+
current = null;
|
|
263
|
+
patchRemoteState({
|
|
264
|
+
type: "offline",
|
|
265
|
+
reason
|
|
266
|
+
});
|
|
267
|
+
connection.close();
|
|
268
|
+
};
|
|
269
|
+
const createConnection = (factory) => {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const { signal } = controller;
|
|
272
|
+
let source = null;
|
|
273
|
+
const untilClosed = (operation) => new Promise((resolve, reject) => {
|
|
274
|
+
const onAbort = () => reject(/* @__PURE__ */ new Error("Remote connection closed"));
|
|
275
|
+
if (signal.aborted) {
|
|
276
|
+
onAbort();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
280
|
+
operation().then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
281
|
+
});
|
|
282
|
+
const retryOptions = {
|
|
283
|
+
...REMOTE_RETRY_OPTIONS,
|
|
284
|
+
shouldRetry: () => !signal.aborted
|
|
285
|
+
};
|
|
286
|
+
const connect = async () => {
|
|
287
|
+
if (signal.aborted) return;
|
|
288
|
+
const result = await tryCatchAsync(async () => await factory({
|
|
289
|
+
onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {
|
|
290
|
+
pull({
|
|
291
|
+
remoteSyncId: newSyncId,
|
|
292
|
+
remoteEventHlcSum,
|
|
293
|
+
includeSelf: false
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
onReconnected: () => {
|
|
297
|
+
sync();
|
|
298
|
+
},
|
|
299
|
+
signal
|
|
300
|
+
}));
|
|
301
|
+
if (signal.aborted) {
|
|
302
|
+
if (result.success) disconnectSource(result.data);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!result.success) {
|
|
306
|
+
console.warn("Failed to create remote source", result.error);
|
|
307
|
+
closeConnection(connection, "INITIALIZATION_FAILED");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
source = result.data;
|
|
311
|
+
patchRemoteState({
|
|
312
|
+
type: "online",
|
|
313
|
+
deSynced: false,
|
|
314
|
+
schemaVersionMismatched: false
|
|
315
|
+
});
|
|
316
|
+
sync();
|
|
317
|
+
};
|
|
318
|
+
let activePull = null;
|
|
319
|
+
const pull = (request) => {
|
|
320
|
+
if (!source || signal.aborted) return Promise.resolve();
|
|
321
|
+
const activeSource = source;
|
|
322
|
+
const remoteSyncId = request?.remoteSyncId;
|
|
323
|
+
if (remoteSyncId !== void 0 && remoteSyncId <= pullSyncId.current) {
|
|
324
|
+
checkRemoteConsistency(remoteSyncId, request?.remoteEventHlcSum ?? null);
|
|
325
|
+
return Promise.resolve();
|
|
326
|
+
}
|
|
327
|
+
if (activePull) {
|
|
328
|
+
if (remoteSyncId !== void 0 && (!activePull.requestedSyncId || activePull.requestedSyncId < remoteSyncId)) activePull.requestedSyncId = remoteSyncId;
|
|
329
|
+
return activePull.promise;
|
|
330
|
+
}
|
|
331
|
+
const nextPull = {
|
|
332
|
+
promise: Promise.resolve(),
|
|
333
|
+
requestedSyncId: null
|
|
334
|
+
};
|
|
335
|
+
nextPull.promise = pullAllEvents(activeSource, {
|
|
336
|
+
afterSyncId: pullSyncId.current,
|
|
337
|
+
excludeNodeId: request?.includeSelf ? void 0 : nodeId
|
|
338
|
+
}).catch((error) => {
|
|
339
|
+
if (signal.aborted) return;
|
|
340
|
+
console.error("Error pulling events. Going offline.", error);
|
|
341
|
+
closeConnection(connection, "REMOTE_PULL_ERROR");
|
|
342
|
+
}).finally(() => {
|
|
343
|
+
if (activePull === nextPull) activePull = null;
|
|
344
|
+
const nextTarget = nextPull.requestedSyncId;
|
|
345
|
+
if (nextTarget && nextTarget > pullSyncId.current) pull({ remoteSyncId: nextTarget });
|
|
346
|
+
});
|
|
347
|
+
activePull = nextPull;
|
|
348
|
+
return nextPull.promise;
|
|
349
|
+
};
|
|
350
|
+
const pullAllEvents = async (activeSource, opts) => {
|
|
351
|
+
let hasMore = true;
|
|
352
|
+
let afterSyncId = opts.afterSyncId;
|
|
353
|
+
while (hasMore) {
|
|
354
|
+
const response = await retryRemoteOperation(() => untilClosed(() => activeSource.pullEvents({
|
|
355
|
+
...opts,
|
|
356
|
+
afterSyncId
|
|
357
|
+
})), retryOptions);
|
|
358
|
+
if (signal.aborted) return;
|
|
359
|
+
hasMore = response.hasMore;
|
|
360
|
+
afterSyncId = response.nextSyncId;
|
|
361
|
+
if (response.events) {
|
|
362
|
+
const { processed } = storage.enqueueRemoteEvents(response.events.map((x) => {
|
|
363
|
+
if (x.schema_version > migrator.currentSchemaVersion) {
|
|
364
|
+
eventTarget.dispatchEvent("remote-schema-version-mismatch", {
|
|
365
|
+
remoteSchemaVersion: x.schema_version,
|
|
366
|
+
localSchemaVersion: migrator.currentSchemaVersion
|
|
367
|
+
});
|
|
368
|
+
if (remoteState.type === "online" && !remoteState.schemaVersionMismatched) patchRemoteState({ schemaVersionMismatched: true });
|
|
369
|
+
throw new SchemaVersionMismatchError(x.schema_version, migrator.currentSchemaVersion);
|
|
370
|
+
}
|
|
371
|
+
return x;
|
|
372
|
+
}));
|
|
373
|
+
await untilClosed(() => processed);
|
|
374
|
+
if (signal.aborted) return;
|
|
375
|
+
}
|
|
376
|
+
if (response.nextSyncId <= pullSyncId.current) break;
|
|
377
|
+
if (response.nextSyncId > pullSyncId.current) pullSyncId.current = response.nextSyncId;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const push = ensureSingletonExecution(async () => {
|
|
381
|
+
while (source && !signal.aborted) {
|
|
382
|
+
const activeSource = source;
|
|
383
|
+
const eventsBatch = storage.getEventsBatch({
|
|
384
|
+
status: "applied",
|
|
385
|
+
afterSyncId: pushSyncId.current,
|
|
386
|
+
excludeOrigin: "remote",
|
|
387
|
+
limit: bufferSize
|
|
388
|
+
});
|
|
389
|
+
if (eventsBatch.events.length === 0) break;
|
|
390
|
+
let response;
|
|
391
|
+
try {
|
|
392
|
+
response = await retryRemoteOperation(() => untilClosed(() => activeSource.pushEvents({
|
|
393
|
+
nodeId,
|
|
394
|
+
events: eventsBatch.events.map((event) => ({
|
|
395
|
+
schema_version: event.schema_version,
|
|
396
|
+
timestamp: event.timestamp,
|
|
397
|
+
type: event.type,
|
|
398
|
+
dataset: event.dataset,
|
|
399
|
+
item_id: event.item_id,
|
|
400
|
+
payload: event.payload
|
|
401
|
+
}))
|
|
402
|
+
})), retryOptions);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!signal.aborted) {
|
|
405
|
+
console.error("Error pushing events. Going offline.", error);
|
|
406
|
+
closeConnection(connection, "REMOTE_PUSH_ERROR");
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (signal.aborted) return;
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
console.error("Remote rejected pushed events. Going offline.");
|
|
413
|
+
closeConnection(connection, "REMOTE_PUSH_ERROR");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
pushSyncId.current = eventsBatch.nextSyncId;
|
|
417
|
+
if (response.beforeSyncId !== void 0 && response.afterSyncId !== void 0 && response.beforeSyncId <= pullSyncId.current && response.afterSyncId > pullSyncId.current) pullSyncId.current = response.afterSyncId;
|
|
418
|
+
if (!eventsBatch.hasMore) break;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
const sync = async () => {
|
|
422
|
+
await activePull?.promise;
|
|
423
|
+
await pull();
|
|
424
|
+
await push.promise();
|
|
425
|
+
await push();
|
|
426
|
+
};
|
|
427
|
+
const close = () => {
|
|
428
|
+
controller.abort();
|
|
429
|
+
if (source) disconnectSource(source);
|
|
430
|
+
};
|
|
431
|
+
const connection = {
|
|
432
|
+
ready: Promise.resolve().then(() => untilClosed(connect)).catch((error) => {
|
|
433
|
+
if (!signal.aborted) throw error;
|
|
434
|
+
}),
|
|
435
|
+
close,
|
|
436
|
+
push,
|
|
437
|
+
sync
|
|
438
|
+
};
|
|
439
|
+
return connection;
|
|
440
|
+
};
|
|
441
|
+
const goOnline = () => {
|
|
442
|
+
if (disposed) return Promise.resolve();
|
|
443
|
+
if (current) return current.ready;
|
|
444
|
+
if (!remoteFactory) {
|
|
445
|
+
console.warn("Remote source factory not provided. Going offline.");
|
|
446
|
+
patchRemoteState({
|
|
447
|
+
type: "offline",
|
|
448
|
+
reason: "NOT_INITIALIZED"
|
|
449
|
+
});
|
|
450
|
+
return Promise.resolve();
|
|
451
|
+
}
|
|
452
|
+
wantsOnline = true;
|
|
453
|
+
const connection = createConnection(remoteFactory);
|
|
454
|
+
current = connection;
|
|
455
|
+
patchRemoteState({ type: "pending" });
|
|
456
|
+
return connection.ready;
|
|
457
|
+
};
|
|
458
|
+
const goOffline = async (reason) => {
|
|
459
|
+
wantsOnline = false;
|
|
460
|
+
if (current) closeConnection(current, reason);
|
|
461
|
+
};
|
|
462
|
+
const syncWithRemote = async () => {
|
|
463
|
+
if (current) await current.sync();
|
|
464
|
+
else if (wantsOnline && !remoteState.schemaVersionMismatched) await goOnline();
|
|
465
|
+
};
|
|
466
|
+
const checkRemoteConsistency = (remoteSyncId, remoteEventHlcSum) => {
|
|
467
|
+
if (remoteEventHlcSum === null) return;
|
|
468
|
+
if (remoteSyncId !== pullSyncId.current) return;
|
|
469
|
+
if (!storage.checkIsQuiescent(pushSyncId.current)) return;
|
|
470
|
+
const localEventHlcSum = storage.getEventHlcAccumulator();
|
|
471
|
+
if (localEventHlcSum === null) return;
|
|
472
|
+
if (localEventHlcSum === remoteEventHlcSum) return;
|
|
473
|
+
eventTarget.dispatchEvent("de-sync-detected", { reason: "CHECKSUM_MISMATCH" });
|
|
474
|
+
console.warn(`[sqlite-sync] De-sync detected at syncId ${remoteSyncId}: local HLC checksum ${localEventHlcSum} != remote ${remoteEventHlcSum}. Local and remote have diverged despite being caught up.`);
|
|
475
|
+
if (remoteState.type === "online" && !remoteState.deSynced) patchRemoteState({ deSynced: true });
|
|
476
|
+
};
|
|
477
|
+
const eventsAppliedSubscription = storage.addEventListener("events-applied", () => {
|
|
478
|
+
current?.push();
|
|
479
|
+
});
|
|
480
|
+
const remoteEventApplyFailedSubscription = storage.addEventListener("remote-event-apply-failed", () => {
|
|
481
|
+
eventTarget.dispatchEvent("de-sync-detected", { reason: "ERROR_APPLYING_REMOTE_EVENT" });
|
|
482
|
+
if (remoteState.type === "online" && !remoteState.deSynced) patchRemoteState({ deSynced: true });
|
|
483
|
+
});
|
|
484
|
+
const getState = () => ({
|
|
485
|
+
remoteState: remoteState.type,
|
|
486
|
+
deSynced: remoteState.deSynced,
|
|
487
|
+
schemaVersionMismatched: remoteState.schemaVersionMismatched
|
|
488
|
+
});
|
|
489
|
+
const dispose = async () => {
|
|
490
|
+
disposed = true;
|
|
491
|
+
eventsAppliedSubscription.unsubscribe();
|
|
492
|
+
remoteEventApplyFailedSubscription.unsubscribe();
|
|
493
|
+
await goOffline("DISCONNECTED");
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
goOnline,
|
|
497
|
+
goOffline,
|
|
498
|
+
syncWithRemote,
|
|
499
|
+
getState,
|
|
500
|
+
dispose,
|
|
501
|
+
addEventListener: eventTarget.addEventListener,
|
|
502
|
+
removeEventListener: eventTarget.removeEventListener
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
//#endregion
|
|
506
|
+
export { HLCCounter as a, serializeHLC as c, DEFAULT_MAX_DRIFT_MS as i, CrdtEventValidationError as n, compareHLC as o, validateNewCrdtEvent as r, deserializeHLC as s, createCrdtSyncRemoteSource as t };
|
|
507
|
+
|
|
508
|
+
//# sourceMappingURL=crdt-sync-remote-source-CugbiosG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt-sync-remote-source-CugbiosG.js","names":[],"sources":["../src/hlc.ts","../src/schema/validate-crdt-event.ts","../src/sqlite-crdt/retry-remote-operation.ts","../src/sqlite-crdt/crdt-sync-remote-source.ts"],"sourcesContent":["export interface HLC {\n timestamp: number;\n counter: number;\n nodeId: string;\n}\n\nconst MAX_COUNTER = 36 ** 5 - 1; // 60,466,175 — max value that fits in 5-char base36\nexport const DEFAULT_MAX_DRIFT_MS = 6 * 60 * 60 * 1000; // 6 hours\n\nexport class HLCCounter {\n private timestamp: number;\n private counter: number;\n private nodeId: string;\n\n private readonly getTimestamp: () => number;\n private readonly maxDrift: number;\n\n constructor(nodeId: string, getTimestamp: () => number, maxDrift: number = DEFAULT_MAX_DRIFT_MS) {\n this.timestamp = getTimestamp();\n this.counter = 0;\n this.nodeId = nodeId;\n this.getTimestamp = getTimestamp;\n this.maxDrift = maxDrift;\n }\n\n getCurrentHLC(): HLC {\n return {\n timestamp: this.timestamp,\n counter: this.counter,\n nodeId: this.nodeId,\n };\n }\n\n getNextHLC(): HLC {\n const now = this.getTimestamp();\n\n if (now > this.timestamp) {\n this.timestamp = now;\n this.counter = 0;\n return this.getCurrentHLC();\n }\n\n this.counter++;\n this.rollOverCounter();\n return this.getCurrentHLC();\n }\n\n /** Restore already-persisted causal state, regardless of wall-clock drift. */\n restoreHLC(hlc: HLC) {\n if (hlc.timestamp > this.timestamp) {\n this.timestamp = hlc.timestamp;\n this.counter = hlc.counter;\n } else if (hlc.timestamp === this.timestamp) {\n this.counter = Math.max(this.counter, hlc.counter);\n }\n }\n\n mergeHLC(hlc: HLC) {\n const now = this.getTimestamp();\n if (hlc.timestamp - now > this.maxDrift) {\n console.warn(\n `HLC: ignoring far-future timestamp (remote=${hlc.timestamp}, local=${now}, drift=${hlc.timestamp - now}ms)`,\n );\n return;\n }\n\n if (this.timestamp === hlc.timestamp) {\n this.counter = Math.max(this.counter, hlc.counter) + 1;\n } else if (this.timestamp > hlc.timestamp) {\n this.counter++;\n } else {\n this.timestamp = hlc.timestamp;\n this.counter = hlc.counter + 1;\n }\n this.rollOverCounter();\n }\n\n private rollOverCounter() {\n if (this.counter > MAX_COUNTER) {\n this.timestamp += 1;\n this.counter = 0;\n }\n }\n}\n\nexport function serializeHLC(hlc: HLC) {\n return `${hlc.timestamp.toString().padStart(15, \"0\")}:${hlc.counter.toString(36).padStart(5, \"0\")}:${hlc.nodeId}`;\n}\n\nexport function deserializeHLC(serialized: string): HLC {\n const parts = serialized.split(\":\");\n if (parts.length < 3) {\n throw new Error(`Invalid HLC format: expected at least 3 colon-separated segments, got ${parts.length}`);\n }\n\n const timestamp = parseInt(parts[0], 10);\n const counter = parseInt(parts[1], 36);\n\n if (Number.isNaN(timestamp) || Number.isNaN(counter)) {\n throw new Error(`Invalid HLC values: timestamp=${parts[0]}, counter=${parts[1]}`);\n }\n\n return {\n timestamp,\n counter,\n nodeId: parts.slice(2).join(\":\"),\n };\n}\n\nexport function compareHLC(one: HLC, two: HLC) {\n if (one.timestamp === two.timestamp) {\n if (one.counter === two.counter) {\n if (one.nodeId === two.nodeId) {\n return 0;\n }\n return one.nodeId < two.nodeId ? -1 : 1;\n }\n return one.counter - two.counter;\n }\n return one.timestamp - two.timestamp;\n}\n","import type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport type { CrdtEventType, NewCrdtEvent } from \"../sqlite-crdt/crdt-table-schema\";\nimport type { AnyTableBuilder } from \"./table-builder\";\n\nexport type NewCrdtEventValidationResult =\n | { success: true; event: NewCrdtEvent }\n | { success: false; errors: string[] };\n\nexport class CrdtEventValidationError extends Error {\n constructor(readonly errors: string[]) {\n super(`Invalid CRDT events: ${errors.join(\"; \")}`);\n this.name = \"CrdtEventValidationError\";\n }\n}\n\nconst crdtEventTypes = [\"item-created\", \"item-updated\", \"item-deleted\"] as const;\n\nexport function validateNewCrdtEvent(\n schema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">,\n input: unknown,\n): NewCrdtEventValidationResult {\n const errors: string[] = [];\n\n if (!isRecord(input)) {\n return { success: false, errors: [\"Event must be an object\"] } satisfies NewCrdtEventValidationResult;\n }\n\n const type = input.type;\n const dataset = input.dataset;\n const itemId = input.item_id;\n\n let payload = input.payload;\n let payloadParseFailed = false;\n if (typeof payload === \"string\") {\n try {\n payload = JSON.parse(payload);\n } catch {\n payloadParseFailed = true;\n errors.push(`Event payload is not valid JSON`);\n }\n }\n\n if (!isCrdtEventType(type)) {\n errors.push(`Invalid event type \"${String(type)}\"`);\n }\n if (typeof dataset !== \"string\") {\n errors.push(`Event dataset must be a string`);\n }\n if (typeof itemId !== \"string\") {\n errors.push(`Event item_id must be a string`);\n }\n if (!payloadParseFailed && !isRecord(payload)) {\n errors.push(`Event payload must be an object`);\n }\n\n const datasetMatch = typeof dataset === \"string\" ? findTableForDataset(schema, dataset) : null;\n if (typeof dataset === \"string\" && !datasetMatch) {\n errors.push(`Unknown dataset \"${dataset}\"`);\n }\n\n if (isCrdtEventType(type) && datasetMatch && isRecord(payload)) {\n switch (type) {\n case \"item-created\": {\n const result = datasetMatch.table.validatePayload(payload, { event: \"item-created\" });\n if (!result.success) {\n errors.push(...result.errors.map((error) => `payload: ${error}`));\n }\n if (typeof itemId === \"string\" && \"id\" in payload && payload.id !== itemId) {\n errors.push(`payload: Column \"id\" must match item_id \"${itemId}\"`);\n }\n break;\n }\n case \"item-updated\": {\n const result = datasetMatch.table.validatePayload(payload, { event: \"item-updated\" });\n if (!result.success) {\n errors.push(...result.errors.map((error) => `payload: ${error}`));\n }\n break;\n }\n case \"item-deleted\":\n if (Object.keys(payload).length > 0) {\n errors.push(`payload: item-deleted events must have an empty payload`);\n }\n break;\n }\n }\n\n if (errors.length > 0) {\n return { success: false, errors } satisfies NewCrdtEventValidationResult;\n }\n\n return {\n success: true,\n event: {\n type: type as CrdtEventType,\n dataset: datasetMatch?.baseTableName ?? (dataset as string),\n item_id: itemId as string,\n payload: payload as Record<string, unknown>,\n },\n } satisfies NewCrdtEventValidationResult;\n}\n\nfunction findTableForDataset(\n schema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">,\n dataset: string,\n): { table: AnyTableBuilder; baseTableName: string } | null {\n const config = schema.tablesConfig.find(\n (tableConfig) => tableConfig.baseTableName === dataset || tableConfig.crdtTableName === dataset,\n );\n if (config) {\n const table = schema.tables[config.crdtTableName];\n return table ? { table, baseTableName: config.baseTableName } : null;\n }\n const table = Object.hasOwn(schema.tables, dataset) ? schema.tables[dataset] : undefined;\n return table ? { table, baseTableName: dataset } : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isCrdtEventType(value: unknown): value is CrdtEventType {\n return typeof value === \"string\" && crdtEventTypes.includes(value as CrdtEventType);\n}\n","type RetryOptions = {\n maxAttempts: number;\n backoffBaseMs: number;\n backoffExponent: number;\n backoffJitterMs: number;\n timeoutMs: number;\n shouldRetry?: (error: unknown) => boolean;\n};\n\nexport const REMOTE_RETRY_OPTIONS: RetryOptions = {\n maxAttempts: 3,\n backoffBaseMs: 100,\n backoffExponent: 1.5,\n backoffJitterMs: 150,\n timeoutMs: 10000,\n};\n\nclass RetryTimeoutError extends Error {\n constructor(\n message: string,\n public previous?: unknown,\n ) {\n super(message);\n this.name = \"TimeoutError\";\n }\n}\n\nconst applyJitter = (delayMs: number, maxJitterMs: number): number => {\n const jitter = Math.random() * maxJitterMs * (Math.random() > 0.5 ? 1 : -1);\n return Math.max(0, delayMs + jitter);\n};\n\nconst delay = (delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs));\n\nconst withTimeout = async <T>(operation: () => Promise<T>, timeoutMs: number, previousError?: unknown): Promise<T> => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n return await Promise.race([\n operation(),\n new Promise<never>((_, reject) => {\n timeoutId = setTimeout(\n () => reject(new RetryTimeoutError(\"Remote operation timed out\", previousError)),\n timeoutMs,\n );\n }),\n ]);\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n};\n\nexport const retryRemoteOperation = async <T>(operation: () => Promise<T>, options: RetryOptions): Promise<T> => {\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {\n try {\n return await withTimeout(operation, options.timeoutMs, lastError);\n } catch (error) {\n lastError = error;\n\n if (attempt >= options.maxAttempts || options.shouldRetry?.(error) === false) {\n throw error;\n }\n\n const backoffDelay = applyJitter(\n options.backoffBaseMs * options.backoffExponent ** (attempt - 1),\n options.backoffJitterMs,\n );\n if (backoffDelay > 0) {\n await delay(backoffDelay);\n }\n }\n }\n\n throw lastError;\n};\n","import type { SyncDbMigrator } from \"../migrations/migrator\";\nimport { createTypedEventTarget, ensureSingletonExecution, tryCatchAsync } from \"../utils\";\nimport type { EventsPullResponse, WorkerState } from \"../worker-db/worker-common\";\nimport type { PendingCrdtEvent } from \"./apply-crdt-event\";\nimport type { CrdtStorage } from \"./crdt-storage\";\nimport { REMOTE_RETRY_OPTIONS, retryRemoteOperation } from \"./retry-remote-operation\";\nimport type { StoredValue } from \"./stored-value\";\n\ntype CrdtSyncRemoteSourceConfig = {\n bufferSize: number;\n storage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: StoredValue<number>;\n pushSyncId: StoredValue<number>;\n nodeId: string;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nexport type EventsPullRequest = {\n afterSyncId: number;\n excludeNodeId?: string;\n};\n\nexport const PUSH_BATCH_SIZE = 75;\nexport const MAX_PUSH_EVENTS = 100;\n\nexport type EventsPushRequest = {\n nodeId: string;\n events: (PendingCrdtEvent & { schema_version: number })[];\n};\nexport type EventsPushResponse = {\n ok: boolean;\n /** Remote sync_id right before the pushed events were enqueued. */\n beforeSyncId?: number;\n /** Remote sync_id right after the pushed events were enqueued. */\n afterSyncId?: number;\n};\n\nconst DISCONNECT_WATCHDOG_MS = 15_000;\n\nexport type CrdtSyncRemoteSource = ReturnType<typeof createCrdtSyncRemoteSource>;\n\nexport type EventsAvailable = {\n newSyncId: number;\n remoteEventHlcSum: string | null;\n};\n\n/**\n * Opens a connection to the remote. Each call must return an independent source: the library calls\n * `disconnect` on every returned source exactly once, including one that resolves after `signal`\n * has aborted. `signal` aborts when the connection attempt is cancelled or the connection is closed.\n * Call `onReconnected` when the transport re-establishes itself without a new source: broadcasts\n * sent while it was down are lost, so the library pulls and pushes to catch up.\n */\nexport type CreateRemoteSourceFactory = (opts: {\n onEventsAvailable: (event: EventsAvailable) => void;\n onReconnected: () => void;\n signal: AbortSignal;\n}) => RemoteSource | Promise<RemoteSource>;\n\ntype RemoteSource = {\n pullEvents: (request: EventsPullRequest) => Promise<EventsPullResponse>;\n pushEvents: (request: EventsPushRequest) => Promise<EventsPushResponse>;\n disconnect?: () => void | Promise<void>;\n};\n\nexport class SchemaVersionMismatchError extends Error {\n constructor(\n public remoteSchemaVersion: number,\n public localSchemaVersion: number,\n ) {\n super(`Schema version mismatch: remote ${remoteSchemaVersion} != local ${localSchemaVersion}`);\n this.name = \"SchemaVersionMismatchError\";\n }\n}\n\ntype RemoteSourceState =\n | {\n type: \"pending\";\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n }\n | {\n type: \"offline\";\n reason: OfflineReason;\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n }\n | {\n type: \"online\";\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n };\n\nexport type OfflineReason =\n | \"NOT_INITIALIZED\"\n | \"INITIALIZATION_FAILED\"\n | \"REMOTE_PUSH_ERROR\"\n | \"REMOTE_PULL_ERROR\"\n | \"DISCONNECTED\";\n\nexport type DeSyncDetectedReason = \"CHECKSUM_MISMATCH\" | \"ERROR_APPLYING_REMOTE_EVENT\";\n\nexport const createCrdtSyncRemoteSource = ({\n bufferSize,\n storage,\n migrator,\n pullSyncId,\n pushSyncId,\n nodeId,\n remoteFactory,\n}: CrdtSyncRemoteSourceConfig) => {\n const eventTarget = createTypedEventTarget<{\n \"state-changed\": RemoteSourceState[\"type\"];\n \"de-sync-detected\": {\n reason: DeSyncDetectedReason;\n };\n \"remote-schema-version-mismatch\": {\n remoteSchemaVersion: number;\n localSchemaVersion: number;\n };\n }>();\n\n let remoteState: RemoteSourceState = {\n type: \"offline\",\n reason: \"NOT_INITIALIZED\",\n deSynced: false,\n schemaVersionMismatched: false,\n };\n\n const patchRemoteState = (state: Partial<RemoteSourceState>) => {\n remoteState = { ...remoteState, ...state } as RemoteSourceState;\n eventTarget.dispatchEvent(\"state-changed\", remoteState.type);\n };\n\n let current: Connection | null = null;\n let disposed = false;\n let wantsOnline = false;\n\n const disconnectSource = (source: RemoteSource) => {\n const watchdog = setTimeout(() => {\n console.error(`Remote source did not disconnect within ${DISCONNECT_WATCHDOG_MS}ms`);\n }, DISCONNECT_WATCHDOG_MS);\n void tryCatchAsync(async () => await source.disconnect?.()).then((result) => {\n clearTimeout(watchdog);\n if (!result.success) {\n console.warn(\"Error while disconnecting from remote source\", result.error);\n }\n });\n };\n\n const closeConnection = (connection: Connection, reason: OfflineReason) => {\n if (current !== connection) {\n return;\n }\n current = null;\n patchRemoteState({ type: \"offline\", reason });\n connection.close();\n };\n\n type Connection = ReturnType<typeof createConnection>;\n const createConnection = (factory: CreateRemoteSourceFactory) => {\n const controller = new AbortController();\n const { signal } = controller;\n let source: RemoteSource | null = null;\n\n const untilClosed = <T>(operation: () => Promise<T>) =>\n new Promise<T>((resolve, reject) => {\n const onAbort = () => reject(new Error(\"Remote connection closed\"));\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n operation()\n .then(resolve, reject)\n .finally(() => signal.removeEventListener(\"abort\", onAbort));\n });\n\n const retryOptions = { ...REMOTE_RETRY_OPTIONS, shouldRetry: () => !signal.aborted };\n\n const connect = async () => {\n if (signal.aborted) {\n return;\n }\n\n const result = await tryCatchAsync(\n async () =>\n await factory({\n onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {\n pull({ remoteSyncId: newSyncId, remoteEventHlcSum, includeSelf: false });\n },\n onReconnected: () => {\n void sync();\n },\n signal,\n }),\n );\n\n if (signal.aborted) {\n if (result.success) {\n disconnectSource(result.data);\n }\n return;\n }\n\n if (!result.success) {\n console.warn(\"Failed to create remote source\", result.error);\n closeConnection(connection, \"INITIALIZATION_FAILED\");\n return;\n }\n\n source = result.data;\n patchRemoteState({ type: \"online\", deSynced: false, schemaVersionMismatched: false });\n void sync();\n };\n\n type ActivePull = { promise: Promise<void>; requestedSyncId: number | null };\n let activePull: ActivePull | null = null;\n const pull = (request?: {\n remoteSyncId?: number;\n remoteEventHlcSum?: string | null;\n includeSelf?: boolean;\n }): Promise<void> => {\n if (!source || signal.aborted) {\n return Promise.resolve();\n }\n const activeSource = source;\n\n const remoteSyncId = request?.remoteSyncId;\n\n if (remoteSyncId !== undefined && remoteSyncId <= pullSyncId.current) {\n // We are already caught up to this broadcast, so there is nothing to pull.\n // This is the quiescent moment to verify we have not diverged from the\n // remote (the check is a no-op unless we are exactly aligned: remoteSyncId\n // === pullSyncId.current).\n checkRemoteConsistency(remoteSyncId, request?.remoteEventHlcSum ?? null);\n return Promise.resolve();\n }\n\n if (activePull) {\n if (remoteSyncId !== undefined && (!activePull.requestedSyncId || activePull.requestedSyncId < remoteSyncId)) {\n activePull.requestedSyncId = remoteSyncId;\n }\n return activePull.promise;\n }\n\n const nextPull: ActivePull = { promise: Promise.resolve(), requestedSyncId: null };\n nextPull.promise = pullAllEvents(activeSource, {\n afterSyncId: pullSyncId.current,\n excludeNodeId: request?.includeSelf ? undefined : nodeId,\n })\n .catch((error) => {\n if (signal.aborted) {\n return;\n }\n console.error(\"Error pulling events. Going offline.\", error);\n closeConnection(connection, \"REMOTE_PULL_ERROR\");\n })\n .finally(() => {\n if (activePull === nextPull) {\n activePull = null;\n }\n\n const nextTarget = nextPull.requestedSyncId;\n if (nextTarget && nextTarget > pullSyncId.current) {\n pull({ remoteSyncId: nextTarget });\n }\n });\n activePull = nextPull;\n return nextPull.promise;\n };\n\n const pullAllEvents = async (activeSource: RemoteSource, opts: EventsPullRequest) => {\n let hasMore = true;\n let afterSyncId = opts.afterSyncId;\n while (hasMore) {\n const response = await retryRemoteOperation(\n () =>\n untilClosed(() =>\n activeSource.pullEvents({\n ...opts,\n afterSyncId,\n }),\n ),\n retryOptions,\n );\n if (signal.aborted) {\n return;\n }\n hasMore = response.hasMore;\n afterSyncId = response.nextSyncId;\n\n if (response.events) {\n const { processed } = storage.enqueueRemoteEvents(\n response.events.map((x) => {\n if (x.schema_version > migrator.currentSchemaVersion) {\n eventTarget.dispatchEvent(\"remote-schema-version-mismatch\", {\n remoteSchemaVersion: x.schema_version,\n localSchemaVersion: migrator.currentSchemaVersion,\n });\n if (remoteState.type === \"online\" && !remoteState.schemaVersionMismatched) {\n patchRemoteState({ schemaVersionMismatched: true });\n }\n throw new SchemaVersionMismatchError(x.schema_version, migrator.currentSchemaVersion);\n }\n return x;\n }),\n );\n await untilClosed(() => processed);\n if (signal.aborted) {\n return;\n }\n }\n if (response.nextSyncId <= pullSyncId.current) {\n break;\n }\n if (response.nextSyncId > pullSyncId.current) {\n pullSyncId.current = response.nextSyncId;\n }\n }\n };\n\n const push = ensureSingletonExecution(async () => {\n while (source && !signal.aborted) {\n const activeSource = source;\n const eventsBatch = storage.getEventsBatch({\n status: \"applied\",\n afterSyncId: pushSyncId.current,\n excludeOrigin: \"remote\",\n limit: bufferSize,\n });\n if (eventsBatch.events.length === 0) {\n break;\n }\n\n let response: EventsPushResponse;\n try {\n response = await retryRemoteOperation(\n () =>\n untilClosed(() =>\n activeSource.pushEvents({\n nodeId,\n events: eventsBatch.events.map((event) => ({\n schema_version: event.schema_version,\n timestamp: event.timestamp,\n type: event.type,\n dataset: event.dataset,\n item_id: event.item_id,\n payload: event.payload,\n })),\n }),\n ),\n retryOptions,\n );\n } catch (error) {\n if (!signal.aborted) {\n console.error(\"Error pushing events. Going offline.\", error);\n closeConnection(connection, \"REMOTE_PUSH_ERROR\");\n }\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n if (!response.ok) {\n console.error(\"Remote rejected pushed events. Going offline.\");\n closeConnection(connection, \"REMOTE_PUSH_ERROR\");\n return;\n }\n\n pushSyncId.current = eventsBatch.nextSyncId;\n\n // Fast-forward the pull cursor: the remote assigns sync ids for the pushed\n // events synchronously, so (beforeSyncId, afterSyncId] contains only this\n // node's own events. If we are caught up to at least beforeSyncId, the skipped\n // range (pullSyncId, afterSyncId] contains only our own events, so there is\n // nothing to pull up to afterSyncId.\n if (\n response.beforeSyncId !== undefined &&\n response.afterSyncId !== undefined &&\n response.beforeSyncId <= pullSyncId.current &&\n response.afterSyncId > pullSyncId.current\n ) {\n pullSyncId.current = response.afterSyncId;\n }\n if (!eventsBatch.hasMore) {\n break;\n }\n }\n });\n\n // Waits out in-flight operations first: they may have started before the caller's request.\n const sync = async () => {\n await activePull?.promise;\n await pull();\n await push.promise();\n await push();\n };\n\n const close = () => {\n controller.abort();\n if (source) {\n disconnectSource(source);\n }\n };\n\n const ready = Promise.resolve()\n .then(() => untilClosed(connect))\n .catch((error) => {\n if (!signal.aborted) {\n throw error;\n }\n });\n\n const connection = { ready, close, push, sync };\n return connection;\n };\n\n const goOnline = (): Promise<void> => {\n if (disposed) {\n return Promise.resolve();\n }\n if (current) {\n return current.ready;\n }\n if (!remoteFactory) {\n console.warn(\"Remote source factory not provided. Going offline.\");\n patchRemoteState({ type: \"offline\", reason: \"NOT_INITIALIZED\" });\n return Promise.resolve();\n }\n\n wantsOnline = true;\n const connection = createConnection(remoteFactory);\n current = connection;\n patchRemoteState({ type: \"pending\" });\n return connection.ready;\n };\n\n const goOffline = async (reason: OfflineReason) => {\n wantsOnline = false;\n if (current) {\n closeConnection(current, reason);\n }\n };\n\n // Reconnects only when a connection error, not goOffline, took us offline. Retrying after a schema\n // mismatch would fail the same way.\n const syncWithRemote = async () => {\n if (current) {\n await current.sync();\n } else if (wantsOnline && !remoteState.schemaVersionMismatched) {\n await goOnline();\n }\n };\n\n // De-sync detection: when we are exactly caught up to the remote's broadcast\n // sync id and fully quiescent, our applied-event set must equal the remote's,\n // so our HLC checksums must match. A mismatch means the nodes have diverged.\n const checkRemoteConsistency = (remoteSyncId: number, remoteEventHlcSum: string | null) => {\n // A remote with no accumulator gives us nothing to compare against.\n if (remoteEventHlcSum === null) {\n return;\n }\n\n // Only meaningful when we are exactly caught up: if we are behind we still\n // need to pull; if we are ahead our state covers events the remote checksum\n // does not.\n if (remoteSyncId !== pullSyncId.current) {\n return;\n }\n\n // Quiescence: the accumulator only matches the remote's when nothing is left\n // to apply locally and no local applied events are still waiting to be pushed\n // (those are in our accumulator but the remote has not seen them yet).\n if (!storage.checkIsQuiescent(pushSyncId.current)) {\n return;\n }\n\n const localEventHlcSum = storage.getEventHlcAccumulator();\n if (localEventHlcSum === null) {\n return;\n }\n\n if (localEventHlcSum === remoteEventHlcSum) {\n // No de-sync detected.\n return;\n }\n\n eventTarget.dispatchEvent(\"de-sync-detected\", { reason: \"CHECKSUM_MISMATCH\" });\n console.warn(\n `[sqlite-sync] De-sync detected at syncId ${remoteSyncId}: local HLC checksum ${localEventHlcSum} != remote ${remoteEventHlcSum}. Local and remote have diverged despite being caught up.`,\n );\n if (remoteState.type === \"online\" && !remoteState.deSynced) {\n patchRemoteState({ deSynced: true });\n }\n };\n\n const eventsAppliedSubscription = storage.addEventListener(\"events-applied\", () => {\n current?.push();\n });\n\n const remoteEventApplyFailedSubscription = storage.addEventListener(\"remote-event-apply-failed\", () => {\n eventTarget.dispatchEvent(\"de-sync-detected\", { reason: \"ERROR_APPLYING_REMOTE_EVENT\" });\n if (remoteState.type === \"online\" && !remoteState.deSynced) {\n patchRemoteState({ deSynced: true });\n }\n });\n\n const getState = (): WorkerState => ({\n remoteState: remoteState.type,\n deSynced: remoteState.deSynced,\n schemaVersionMismatched: remoteState.schemaVersionMismatched,\n });\n\n const dispose = async () => {\n disposed = true;\n eventsAppliedSubscription.unsubscribe();\n remoteEventApplyFailedSubscription.unsubscribe();\n await goOffline(\"DISCONNECTED\");\n };\n\n return {\n goOnline,\n goOffline,\n syncWithRemote,\n getState,\n dispose,\n addEventListener: eventTarget.addEventListener,\n removeEventListener: eventTarget.removeEventListener,\n };\n};\n"],"mappings":"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"}
|
|
@@ -138,6 +138,13 @@ function parseTableName(tableName) {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
//#endregion
|
|
141
|
-
|
|
141
|
+
//#region src/sqlite-crdt/crdt-table-schema.ts
|
|
142
|
+
/** Persisted on applied events that were accepted but did not mutate materialized state. */
|
|
143
|
+
const CRDT_EVENT_NO_OP_PAYLOAD = "no-op";
|
|
144
|
+
function isNoOpCrdtEventPayload(payload) {
|
|
145
|
+
return payload === CRDT_EVENT_NO_OP_PAYLOAD;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { createDeferredPromise as a, generateId as c, parseTableName as d, quoteId as f, TypedEvent as i, jsonSafeParse as l, tryCatchAsync as m, isNoOpCrdtEventPayload as n, createTypedEventTarget as o, tryCatch as p, TypedBroadcastChannel as r, ensureSingletonExecution as s, CRDT_EVENT_NO_OP_PAYLOAD as t, noop as u };
|
|
142
149
|
|
|
143
|
-
//# sourceMappingURL=
|
|
150
|
+
//# sourceMappingURL=crdt-table-schema-2w_AIpsV.js.map
|