@sqlite-sync/core 0.0.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/chunk-LK5FJCUD.js +522 -0
- package/dist/chunk-LK5FJCUD.js.map +1 -0
- package/dist/chunk-YLXMST5Z.js +490 -0
- package/dist/chunk-YLXMST5Z.js.map +1 -0
- package/dist/crdt-sync-producer-0toEpGf0.d.ts +15 -0
- package/dist/crdt-sync-remote-source-rrqinqLn.d.ts +271 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +710 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/worker.d.ts +19 -0
- package/dist/worker.js +261 -0
- package/dist/worker.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SQLiteDbWrapper,
|
|
3
|
+
SqliteDriver,
|
|
4
|
+
applyMemoryDbSchema,
|
|
5
|
+
applyWorkerDbSchema,
|
|
6
|
+
broadcastChannelNames,
|
|
7
|
+
createBroadcastChannels,
|
|
8
|
+
createCrdtSyncRemoteSource,
|
|
9
|
+
createSQLiteKysely,
|
|
10
|
+
createSyncDbMigrations,
|
|
11
|
+
createSyncDbMigrator,
|
|
12
|
+
introspectDb,
|
|
13
|
+
isWorkerInitMessage,
|
|
14
|
+
isWorkerInitResponse,
|
|
15
|
+
isWorkerNotificationMessage,
|
|
16
|
+
isWorkerRequestMessage,
|
|
17
|
+
isWorkerResponseMessage,
|
|
18
|
+
startPerformanceLogger,
|
|
19
|
+
syncDbWorkerLockName
|
|
20
|
+
} from "./chunk-LK5FJCUD.js";
|
|
21
|
+
import {
|
|
22
|
+
TypedBroadcastChannel,
|
|
23
|
+
TypedEvent,
|
|
24
|
+
applyCrdtEventMutations,
|
|
25
|
+
crdtSchema,
|
|
26
|
+
createAsyncAutoFlushBuffer,
|
|
27
|
+
createAutoFlushBuffer,
|
|
28
|
+
createCrdtStorage,
|
|
29
|
+
createCrdtSyncProducer,
|
|
30
|
+
createDeferredPromise,
|
|
31
|
+
createSyncIdCounter,
|
|
32
|
+
createTypedEventTarget,
|
|
33
|
+
dummyKysely,
|
|
34
|
+
ensureSingletonExecution,
|
|
35
|
+
generateId,
|
|
36
|
+
jsonSafeParse,
|
|
37
|
+
orderBy,
|
|
38
|
+
registerCrdtFunctions
|
|
39
|
+
} from "./chunk-YLXMST5Z.js";
|
|
40
|
+
|
|
41
|
+
// src/hlc.ts
|
|
42
|
+
var HLCCounter = class {
|
|
43
|
+
timestamp;
|
|
44
|
+
counter;
|
|
45
|
+
nodeId;
|
|
46
|
+
getTimestamp;
|
|
47
|
+
constructor(nodeId, getTimestamp) {
|
|
48
|
+
this.timestamp = getTimestamp();
|
|
49
|
+
this.counter = 0;
|
|
50
|
+
this.nodeId = nodeId;
|
|
51
|
+
this.getTimestamp = getTimestamp;
|
|
52
|
+
}
|
|
53
|
+
getCurrentHLC() {
|
|
54
|
+
return {
|
|
55
|
+
timestamp: this.timestamp,
|
|
56
|
+
counter: this.counter,
|
|
57
|
+
nodeId: this.nodeId
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
getNextHLC() {
|
|
61
|
+
const now = this.getTimestamp();
|
|
62
|
+
if (now > this.timestamp) {
|
|
63
|
+
this.timestamp = now;
|
|
64
|
+
this.counter = 0;
|
|
65
|
+
return this.getCurrentHLC();
|
|
66
|
+
}
|
|
67
|
+
this.counter++;
|
|
68
|
+
return this.getCurrentHLC();
|
|
69
|
+
}
|
|
70
|
+
mergeHLC(hlc) {
|
|
71
|
+
if (this.timestamp === hlc.timestamp) {
|
|
72
|
+
this.counter = Math.max(this.counter, hlc.counter) + 1;
|
|
73
|
+
} else if (this.timestamp > hlc.timestamp) {
|
|
74
|
+
this.counter++;
|
|
75
|
+
} else {
|
|
76
|
+
this.timestamp = hlc.timestamp;
|
|
77
|
+
this.counter = hlc.counter + 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function serializeHLC(hlc) {
|
|
82
|
+
return hlc.timestamp.toString().padStart(15, "0") + ":" + hlc.counter.toString(36).padStart(5, "0") + ":" + hlc.nodeId;
|
|
83
|
+
}
|
|
84
|
+
function deserializeHLC(serialized) {
|
|
85
|
+
const [ts, count, ...node] = serialized.split(":");
|
|
86
|
+
return {
|
|
87
|
+
timestamp: parseInt(ts),
|
|
88
|
+
counter: parseInt(count, 36),
|
|
89
|
+
nodeId: node.join(":")
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function compareHLC(one, two) {
|
|
93
|
+
if (one.timestamp == two.timestamp) {
|
|
94
|
+
if (one.counter === two.counter) {
|
|
95
|
+
if (one.nodeId === two.nodeId) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
return one.nodeId < two.nodeId ? -1 : 1;
|
|
99
|
+
}
|
|
100
|
+
return one.counter - two.counter;
|
|
101
|
+
}
|
|
102
|
+
return one.timestamp - two.timestamp;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/worker-db/db-worker-client.ts
|
|
106
|
+
var createWorkerDbClient = ({
|
|
107
|
+
broadcastChannels
|
|
108
|
+
}) => {
|
|
109
|
+
const eventTarget = createTypedEventTarget();
|
|
110
|
+
const workerRequestsMap = /* @__PURE__ */ new Map();
|
|
111
|
+
const queryWorker = (method, args) => {
|
|
112
|
+
const requestId = crypto.randomUUID();
|
|
113
|
+
const promise = createDeferredPromise();
|
|
114
|
+
workerRequestsMap.set(requestId, promise);
|
|
115
|
+
const request = {
|
|
116
|
+
type: "request",
|
|
117
|
+
requestId,
|
|
118
|
+
method,
|
|
119
|
+
args
|
|
120
|
+
};
|
|
121
|
+
broadcastChannels.requests.postMessage(request);
|
|
122
|
+
return promise.promise;
|
|
123
|
+
};
|
|
124
|
+
const handleWorkerResponse = (message) => {
|
|
125
|
+
const promise = workerRequestsMap.get(message.requestId);
|
|
126
|
+
if (!promise) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
promise.resolve(message.data);
|
|
130
|
+
workerRequestsMap.delete(message.requestId);
|
|
131
|
+
};
|
|
132
|
+
broadcastChannels.responses.onmessage = (event) => {
|
|
133
|
+
const message = event.data;
|
|
134
|
+
if (isWorkerResponseMessage(message)) {
|
|
135
|
+
handleWorkerResponse(message);
|
|
136
|
+
} else if (isWorkerNotificationMessage(message)) {
|
|
137
|
+
eventTarget.dispatchEvent("new-notification", message);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const rpc = {
|
|
141
|
+
execute: (query) => queryWorker("execute", [query]),
|
|
142
|
+
getSnapshot: () => queryWorker("getSnapshot", []),
|
|
143
|
+
pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
|
|
144
|
+
pullEvents: (params) => queryWorker("pullEvents", [params]),
|
|
145
|
+
postInitReady: () => queryWorker("postInitReady", [])
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
...rpc,
|
|
149
|
+
addEventListener: eventTarget.addEventListener,
|
|
150
|
+
removeEventListener: eventTarget.removeEventListener
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
function initializeWorkerDb({
|
|
154
|
+
worker,
|
|
155
|
+
broadcastChannels,
|
|
156
|
+
config
|
|
157
|
+
}) {
|
|
158
|
+
const promise = createDeferredPromise();
|
|
159
|
+
broadcastChannels.responses.onmessage = (event) => {
|
|
160
|
+
const message = event.data;
|
|
161
|
+
if (!isWorkerInitResponse(message)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
promise.resolve();
|
|
165
|
+
worker.onmessage = null;
|
|
166
|
+
};
|
|
167
|
+
const configMessage = {
|
|
168
|
+
type: "init",
|
|
169
|
+
config
|
|
170
|
+
};
|
|
171
|
+
worker.postMessage(configMessage);
|
|
172
|
+
broadcastChannels.requests.postMessage({
|
|
173
|
+
type: "request",
|
|
174
|
+
requestId: crypto.randomUUID(),
|
|
175
|
+
method: "postInitReady",
|
|
176
|
+
args: []
|
|
177
|
+
});
|
|
178
|
+
return promise.promise;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/memory-db/sqlite-reactive-db.ts
|
|
182
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
183
|
+
var sqliteModule = null;
|
|
184
|
+
function createSQLiteReactiveDb(opts) {
|
|
185
|
+
return SQLiteReactiveDb.create(opts);
|
|
186
|
+
}
|
|
187
|
+
var defaultLogger = (type, message, level = "info") => {
|
|
188
|
+
const logMessage = `[${type}] ${message}`;
|
|
189
|
+
switch (level) {
|
|
190
|
+
case "info":
|
|
191
|
+
console.log(logMessage);
|
|
192
|
+
break;
|
|
193
|
+
case "warning":
|
|
194
|
+
console.warn(logMessage);
|
|
195
|
+
break;
|
|
196
|
+
case "error":
|
|
197
|
+
console.error(logMessage);
|
|
198
|
+
break;
|
|
199
|
+
case "trace":
|
|
200
|
+
console.trace(logMessage);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
205
|
+
db;
|
|
206
|
+
sqlite3;
|
|
207
|
+
logger;
|
|
208
|
+
tablesUsedStatement = null;
|
|
209
|
+
eventTarget = createTypedEventTarget();
|
|
210
|
+
constructor(sqlite3, logger) {
|
|
211
|
+
this.sqlite3 = sqlite3;
|
|
212
|
+
this.logger = logger;
|
|
213
|
+
this.db = new SQLiteDbWrapper({
|
|
214
|
+
db: new sqlite3.oo1.DB({ filename: ":memory:" }),
|
|
215
|
+
logger: this.logger,
|
|
216
|
+
loggerPrefix: "memory",
|
|
217
|
+
sqlite3
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
static async create(opts) {
|
|
221
|
+
const logger = opts.logger ?? defaultLogger;
|
|
222
|
+
const perf = startPerformanceLogger(logger);
|
|
223
|
+
if (!sqliteModule) {
|
|
224
|
+
sqliteModule = await sqlite3InitModule();
|
|
225
|
+
}
|
|
226
|
+
const db = new _SQLiteReactiveDb(sqliteModule, logger);
|
|
227
|
+
if (opts.snapshot) {
|
|
228
|
+
db.useSnapshot(opts.snapshot);
|
|
229
|
+
}
|
|
230
|
+
db.registerDbHooks();
|
|
231
|
+
perf.logEnd("createSQLiteMemoryDb", "success", "info");
|
|
232
|
+
return db;
|
|
233
|
+
}
|
|
234
|
+
createLiveQuery(query) {
|
|
235
|
+
const fetchRows = () => this.db.execute({
|
|
236
|
+
sql: query.sql,
|
|
237
|
+
parameters: query.parameters ?? []
|
|
238
|
+
}).rows;
|
|
239
|
+
let rows = null;
|
|
240
|
+
const getRows = () => {
|
|
241
|
+
if (!rows) {
|
|
242
|
+
rows = fetchRows();
|
|
243
|
+
}
|
|
244
|
+
return rows;
|
|
245
|
+
};
|
|
246
|
+
let subscriber = null;
|
|
247
|
+
const refresh = () => {
|
|
248
|
+
rows = fetchRows();
|
|
249
|
+
subscriber?.();
|
|
250
|
+
};
|
|
251
|
+
const subscribe = (onchange) => {
|
|
252
|
+
if (subscriber) {
|
|
253
|
+
throw new Error("Subscriber already exists");
|
|
254
|
+
}
|
|
255
|
+
subscriber = onchange;
|
|
256
|
+
const subscription = this.subscribeToQueryChanges({
|
|
257
|
+
sql: query.sql,
|
|
258
|
+
onDataChange: refresh
|
|
259
|
+
});
|
|
260
|
+
return () => {
|
|
261
|
+
subscription.unsubscribe();
|
|
262
|
+
subscriber = null;
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
return { getRows, refresh, subscribe };
|
|
266
|
+
}
|
|
267
|
+
subscribeToQueryChanges(params) {
|
|
268
|
+
const { sql: sql2, onDataChange } = params;
|
|
269
|
+
const tables = this.getTablesUsed(sql2);
|
|
270
|
+
const readTables = /* @__PURE__ */ new Set();
|
|
271
|
+
for (const table of tables) {
|
|
272
|
+
if (!readTables.has(table.name)) {
|
|
273
|
+
readTables.add(table.name);
|
|
274
|
+
} else if (table.isWrite) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"This query writes and reads from the same table. This may cause infinite loops."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const notifyDataChange = createDebouncedCallback(() => {
|
|
281
|
+
onDataChange();
|
|
282
|
+
}, 30);
|
|
283
|
+
for (const table of readTables) {
|
|
284
|
+
this.eventTarget.addEventListener(`table:${table}`, notifyDataChange);
|
|
285
|
+
}
|
|
286
|
+
this.eventTarget.addEventListener("any-table-changed", notifyDataChange);
|
|
287
|
+
return {
|
|
288
|
+
unsubscribe: () => {
|
|
289
|
+
for (const table of readTables) {
|
|
290
|
+
this.eventTarget.removeEventListener(
|
|
291
|
+
`table:${table}`,
|
|
292
|
+
notifyDataChange
|
|
293
|
+
);
|
|
294
|
+
this.eventTarget.removeEventListener(
|
|
295
|
+
"any-table-changed",
|
|
296
|
+
notifyDataChange
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
subscribeToTableChanges(table, onChanges) {
|
|
303
|
+
this.eventTarget.addEventListener(`table:${table}`, onChanges);
|
|
304
|
+
this.eventTarget.addEventListener("any-table-changed", onChanges);
|
|
305
|
+
return {
|
|
306
|
+
unsubscribe: () => {
|
|
307
|
+
this.eventTarget.removeEventListener(`table:${table}`, onChanges);
|
|
308
|
+
this.eventTarget.removeEventListener("any-table-changed", onChanges);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
getTablesUsed(query) {
|
|
313
|
+
if (!this.tablesUsedStatement) {
|
|
314
|
+
this.tablesUsedStatement = this.db.prepare(
|
|
315
|
+
"select t.tbl_name as name, u.wr as isWrite from tables_used(?) as u inner join sqlite_master as t on t.name = u.name where u.schema = 'main'"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const tables = this.tablesUsedStatement.execute([query]);
|
|
319
|
+
if (tables.length == 0 && query.toLowerCase().includes("delete")) {
|
|
320
|
+
tables.push(...this.getClearedTables(query));
|
|
321
|
+
}
|
|
322
|
+
return tables;
|
|
323
|
+
}
|
|
324
|
+
getClearedTables(query) {
|
|
325
|
+
const operations = this.db.execute(`EXPLAIN ${query.split(";")[0]}`).rows;
|
|
326
|
+
const clearedTablesRootPages = /* @__PURE__ */ new Set();
|
|
327
|
+
for (const operation of operations) {
|
|
328
|
+
if (operation.opcode === "Clear" && operation.p2 === 0) {
|
|
329
|
+
clearedTablesRootPages.add(operation.p1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (clearedTablesRootPages.size === 0) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const tableNames = this.db.execute(
|
|
336
|
+
`select t.tbl_name as name, true as isWrite from sqlite_master as t where t.rootpage in (${Array.from(
|
|
337
|
+
clearedTablesRootPages
|
|
338
|
+
).join(",")})`
|
|
339
|
+
).rows;
|
|
340
|
+
return tableNames;
|
|
341
|
+
}
|
|
342
|
+
addEventListener(type, listener) {
|
|
343
|
+
this.eventTarget.addEventListener(type, listener);
|
|
344
|
+
}
|
|
345
|
+
removeEventListener(type, listener) {
|
|
346
|
+
this.eventTarget.removeEventListener(type, listener);
|
|
347
|
+
}
|
|
348
|
+
notifyTableSubscribers(tables = []) {
|
|
349
|
+
if (tables.length === 0) {
|
|
350
|
+
this.eventTarget.dispatchEvent("any-table-changed", void 0);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
for (const table of tables) {
|
|
354
|
+
this.eventTarget.dispatchEvent(`table:${table}`, void 0);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
registerDbHooks() {
|
|
358
|
+
const updateQueue = /* @__PURE__ */ new Set();
|
|
359
|
+
this.sqlite3.capi.sqlite3_update_hook(
|
|
360
|
+
this.db.ensureDb,
|
|
361
|
+
(_ctx, _opId, _db, table) => {
|
|
362
|
+
updateQueue.add(table);
|
|
363
|
+
},
|
|
364
|
+
0
|
|
365
|
+
);
|
|
366
|
+
this.sqlite3.capi.sqlite3_rollback_hook(
|
|
367
|
+
this.db.ensureDb,
|
|
368
|
+
() => {
|
|
369
|
+
if (updateQueue.size === 0) {
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
updateQueue.clear();
|
|
373
|
+
this.eventTarget.dispatchEvent("transaction-rolled-back", void 0);
|
|
374
|
+
return 0;
|
|
375
|
+
},
|
|
376
|
+
0
|
|
377
|
+
);
|
|
378
|
+
this.sqlite3.capi.sqlite3_commit_hook(
|
|
379
|
+
this.db.ensureDb,
|
|
380
|
+
() => {
|
|
381
|
+
if (updateQueue.size === 0) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
const tables = Array.from(updateQueue);
|
|
385
|
+
updateQueue.clear();
|
|
386
|
+
this.eventTarget.dispatchEvent("transaction-committed", void 0);
|
|
387
|
+
queueMicrotask(() => {
|
|
388
|
+
this.notifyTableSubscribers(tables);
|
|
389
|
+
});
|
|
390
|
+
return 0;
|
|
391
|
+
},
|
|
392
|
+
0
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
createSnapshot() {
|
|
396
|
+
const perf = startPerformanceLogger(this.logger);
|
|
397
|
+
const snapshot = this.sqlite3.capi.sqlite3_js_db_export(this.db.ensureDb);
|
|
398
|
+
perf.logEnd(
|
|
399
|
+
"createSnapshot",
|
|
400
|
+
`snapshot size: ${snapshot.byteLength}`,
|
|
401
|
+
"info"
|
|
402
|
+
);
|
|
403
|
+
return snapshot;
|
|
404
|
+
}
|
|
405
|
+
useSnapshot(snapshot) {
|
|
406
|
+
this.db.useSnapshot(snapshot);
|
|
407
|
+
this.notifyTableSubscribers();
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
function createDebouncedCallback(callback, delay) {
|
|
411
|
+
let timeout = null;
|
|
412
|
+
let shouldCallWithoutDelay = true;
|
|
413
|
+
return (...args) => {
|
|
414
|
+
if (shouldCallWithoutDelay) {
|
|
415
|
+
callback(...args);
|
|
416
|
+
shouldCallWithoutDelay = false;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const effect = () => {
|
|
420
|
+
timeout = null;
|
|
421
|
+
shouldCallWithoutDelay = true;
|
|
422
|
+
return callback(...args);
|
|
423
|
+
};
|
|
424
|
+
if (timeout) {
|
|
425
|
+
clearTimeout(timeout);
|
|
426
|
+
}
|
|
427
|
+
timeout = setTimeout(effect, delay);
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/memory-db/memory-db.ts
|
|
432
|
+
import { sql } from "kysely";
|
|
433
|
+
|
|
434
|
+
// src/sqlite-crdt/make-crdt-table.ts
|
|
435
|
+
function makeCrdtTable({
|
|
436
|
+
db,
|
|
437
|
+
baseTableName,
|
|
438
|
+
crdtTableName
|
|
439
|
+
}) {
|
|
440
|
+
const tableSchema = db.dbSchema[baseTableName];
|
|
441
|
+
if (!tableSchema) {
|
|
442
|
+
throw new Error(`Table ${baseTableName} not found`);
|
|
443
|
+
}
|
|
444
|
+
db.execute(`
|
|
445
|
+
create view ${crdtTableName} as
|
|
446
|
+
select * from ${baseTableName}
|
|
447
|
+
where tombstone = 0;`);
|
|
448
|
+
const allColumnNames = tableSchema.columns.map((column) => column.name);
|
|
449
|
+
const jsonPayload = (from) => "'{'||" + allColumnNames.map((col) => `'"${col}":'||json_quote(${from}.${col})`).join("||','||") + "||'}'";
|
|
450
|
+
db.execute(`
|
|
451
|
+
create trigger ${crdtTableName}_created
|
|
452
|
+
instead of insert on ${crdtTableName}
|
|
453
|
+
for each row
|
|
454
|
+
begin
|
|
455
|
+
select handle_item_created('${baseTableName}', ${jsonPayload("new")});
|
|
456
|
+
end;
|
|
457
|
+
`);
|
|
458
|
+
db.execute(`
|
|
459
|
+
create trigger ${crdtTableName}_updated
|
|
460
|
+
instead of update on ${crdtTableName}
|
|
461
|
+
for each row
|
|
462
|
+
begin
|
|
463
|
+
select handle_item_updated(
|
|
464
|
+
'${baseTableName}',
|
|
465
|
+
${jsonPayload("old")},
|
|
466
|
+
${jsonPayload("new")}
|
|
467
|
+
);
|
|
468
|
+
end;
|
|
469
|
+
`);
|
|
470
|
+
db.execute(`
|
|
471
|
+
create trigger ${crdtTableName}_deleted
|
|
472
|
+
instead of delete on ${crdtTableName}
|
|
473
|
+
for each row
|
|
474
|
+
when old.tombstone = 0
|
|
475
|
+
begin
|
|
476
|
+
select handle_item_deleted('${baseTableName}', old.id);
|
|
477
|
+
end;
|
|
478
|
+
`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/memory-db/memory-db.ts
|
|
482
|
+
async function createMemoryDb({
|
|
483
|
+
reactiveDb: _reactiveDb,
|
|
484
|
+
hlcCounter,
|
|
485
|
+
tabId,
|
|
486
|
+
crdtTables
|
|
487
|
+
}) {
|
|
488
|
+
const reactiveDb = _reactiveDb;
|
|
489
|
+
const db = reactiveDb.db;
|
|
490
|
+
applyMemoryDbSchema(db);
|
|
491
|
+
for (const table of crdtTables) {
|
|
492
|
+
makeCrdtTable({
|
|
493
|
+
db,
|
|
494
|
+
baseTableName: table.baseTableName,
|
|
495
|
+
crdtTableName: table.crdtTableName
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const localSyncId = createSyncIdCounter({
|
|
499
|
+
initialSyncId: 0
|
|
500
|
+
});
|
|
501
|
+
const pendingLocalEvents = [];
|
|
502
|
+
registerCrdtFunctions({
|
|
503
|
+
db,
|
|
504
|
+
getTableSchema: (dataset) => db.dbSchema[dataset],
|
|
505
|
+
getNextTimestamp: () => serializeHLC(hlcCounter.getNextHLC()),
|
|
506
|
+
updateLogTableName: "crdt_update_log",
|
|
507
|
+
onEventApplied: (event) => {
|
|
508
|
+
const persistedEvent = {
|
|
509
|
+
...event,
|
|
510
|
+
origin: tabId,
|
|
511
|
+
sync_id: ++localSyncId.current,
|
|
512
|
+
status: "applied"
|
|
513
|
+
};
|
|
514
|
+
enqueueCrdtEvent(db, persistedEvent);
|
|
515
|
+
pendingLocalEvents.push(persistedEvent);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
db.createScalarFunction({
|
|
519
|
+
name: "gen_id",
|
|
520
|
+
callback: () => generateId(),
|
|
521
|
+
deterministic: false,
|
|
522
|
+
directOnly: false,
|
|
523
|
+
innocuous: true
|
|
524
|
+
});
|
|
525
|
+
reactiveDb.addEventListener("transaction-rolled-back", () => {
|
|
526
|
+
pendingLocalEvents.length = 0;
|
|
527
|
+
});
|
|
528
|
+
reactiveDb.addEventListener("transaction-committed", () => {
|
|
529
|
+
const appliedEvents = pendingLocalEvents.splice(0);
|
|
530
|
+
queueMicrotask(() => {
|
|
531
|
+
for (const event of appliedEvents) {
|
|
532
|
+
crdtStorage.dispatchEvent("event-applied", event);
|
|
533
|
+
}
|
|
534
|
+
crdtStorage.dispatchEvent("event-processing-done", void 0);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
const crdtStorage = createCrdtStorage({
|
|
538
|
+
syncId: localSyncId,
|
|
539
|
+
persistEvents: (events) => persistEvents(db, events),
|
|
540
|
+
popPendingEventsBatch: () => popPendingEventsBatch(db, 50),
|
|
541
|
+
applyCrdtEventMutations: (event) => applyCrdtEventMutations({
|
|
542
|
+
db,
|
|
543
|
+
event,
|
|
544
|
+
updateLogTableName: "crdt_update_log"
|
|
545
|
+
}),
|
|
546
|
+
updateEventStatus: (syncId, status) => updateEventStatus(db, syncId, status)
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
crdtStorage
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function enqueueCrdtEvent(db, event) {
|
|
553
|
+
db.executePrepared(
|
|
554
|
+
"enqueue-crdt-events",
|
|
555
|
+
event,
|
|
556
|
+
(db2, params) => db2.insertInto("persisted_crdt_events").values({
|
|
557
|
+
status: params("status"),
|
|
558
|
+
sync_id: params("sync_id"),
|
|
559
|
+
type: params("type"),
|
|
560
|
+
timestamp: params("timestamp"),
|
|
561
|
+
dataset: params("dataset"),
|
|
562
|
+
item_id: params("item_id"),
|
|
563
|
+
payload: params("payload"),
|
|
564
|
+
origin: params("origin")
|
|
565
|
+
})
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
function persistEvents(db, events) {
|
|
569
|
+
db.executeTransaction((db2) => {
|
|
570
|
+
const chunkSize = 100;
|
|
571
|
+
for (let i = 0; i < events.length; i += chunkSize) {
|
|
572
|
+
const chunk = events.slice(i, i + chunkSize);
|
|
573
|
+
db2.executeKysely(
|
|
574
|
+
(db3) => db3.insertInto("persisted_crdt_events").values(chunk)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function popPendingEventsBatch(db, limit) {
|
|
580
|
+
const events = db.executePrepared(
|
|
581
|
+
"pop-enqueued-crdt-events",
|
|
582
|
+
{
|
|
583
|
+
limit
|
|
584
|
+
},
|
|
585
|
+
(db2, param) => db2.selectFrom("persisted_crdt_events").where("status", "=", sql.lit("pending")).limit(param("limit")).orderBy("sync_id", "asc").selectAll()
|
|
586
|
+
);
|
|
587
|
+
return {
|
|
588
|
+
events,
|
|
589
|
+
hasMore: events.length === limit
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function updateEventStatus(db, syncId, status) {
|
|
593
|
+
db.executePrepared(
|
|
594
|
+
"update-crdt-event-status",
|
|
595
|
+
{ syncId, status },
|
|
596
|
+
(db2, params) => db2.updateTable("persisted_crdt_events").set({ status: params("status") }).where("sync_id", "=", params("syncId"))
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/sync-db.ts
|
|
601
|
+
async function createSyncedDb(options) {
|
|
602
|
+
if (!options.dbPath.startsWith("/")) {
|
|
603
|
+
throw new Error("dbPath must be an absolute path");
|
|
604
|
+
}
|
|
605
|
+
const tabId = generateId();
|
|
606
|
+
const broadcastChannels = createBroadcastChannels();
|
|
607
|
+
await initializeWorkerDb({
|
|
608
|
+
worker: options.worker,
|
|
609
|
+
broadcastChannels,
|
|
610
|
+
config: {
|
|
611
|
+
clientId: generateId(),
|
|
612
|
+
dbPath: options.dbPath,
|
|
613
|
+
clearOnInit: options.clearOnInit,
|
|
614
|
+
syncServer: {
|
|
615
|
+
host: "",
|
|
616
|
+
room: ""
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
const workerClient = createWorkerDbClient({
|
|
621
|
+
broadcastChannels
|
|
622
|
+
});
|
|
623
|
+
const hlcCounter = new HLCCounter(tabId, () => Date.now());
|
|
624
|
+
const workerClientSnapshot = await workerClient.getSnapshot();
|
|
625
|
+
const reactiveDb = await createSQLiteReactiveDb({
|
|
626
|
+
snapshot: workerClientSnapshot.file
|
|
627
|
+
});
|
|
628
|
+
const { crdtStorage } = await createMemoryDb({
|
|
629
|
+
reactiveDb,
|
|
630
|
+
hlcCounter,
|
|
631
|
+
tabId,
|
|
632
|
+
crdtTables: options.crdtTables
|
|
633
|
+
});
|
|
634
|
+
const remoteSyncId = createSyncIdCounter({
|
|
635
|
+
initialSyncId: workerClientSnapshot.syncId
|
|
636
|
+
});
|
|
637
|
+
const remoteSyncSource = createCrdtSyncRemoteSource({
|
|
638
|
+
bufferSize: 100,
|
|
639
|
+
syncId: remoteSyncId,
|
|
640
|
+
storage: crdtStorage,
|
|
641
|
+
nodeId: tabId,
|
|
642
|
+
pullEvents: (request) => workerClient.pullEvents(request),
|
|
643
|
+
pushEvents: (request) => workerClient.pushTabEvents(request)
|
|
644
|
+
});
|
|
645
|
+
workerClient.addEventListener("new-notification", (event) => {
|
|
646
|
+
const notification = event.payload;
|
|
647
|
+
if (notification.notificationType === "new-event-chunk-applied" && notification.newSyncId > remoteSyncId.current) {
|
|
648
|
+
remoteSyncSource.pullEvents();
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
crdtStorage.addEventListener("event-applied", (event) => {
|
|
652
|
+
if (event.payload.origin === "remote") {
|
|
653
|
+
hlcCounter.mergeHLC(deserializeHLC(event.payload.timestamp));
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
db: reactiveDb.db,
|
|
658
|
+
reactiveDb,
|
|
659
|
+
workerDb: workerClient
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
export {
|
|
663
|
+
HLCCounter,
|
|
664
|
+
SQLiteDbWrapper,
|
|
665
|
+
SQLiteReactiveDb,
|
|
666
|
+
SqliteDriver,
|
|
667
|
+
TypedBroadcastChannel,
|
|
668
|
+
TypedEvent,
|
|
669
|
+
applyCrdtEventMutations,
|
|
670
|
+
applyMemoryDbSchema,
|
|
671
|
+
applyWorkerDbSchema,
|
|
672
|
+
broadcastChannelNames,
|
|
673
|
+
compareHLC,
|
|
674
|
+
crdtSchema,
|
|
675
|
+
createAsyncAutoFlushBuffer,
|
|
676
|
+
createAutoFlushBuffer,
|
|
677
|
+
createBroadcastChannels,
|
|
678
|
+
createCrdtStorage,
|
|
679
|
+
createCrdtSyncProducer,
|
|
680
|
+
createCrdtSyncRemoteSource,
|
|
681
|
+
createDeferredPromise,
|
|
682
|
+
createMemoryDb,
|
|
683
|
+
createSQLiteKysely,
|
|
684
|
+
createSQLiteReactiveDb,
|
|
685
|
+
createSyncDbMigrations,
|
|
686
|
+
createSyncDbMigrator,
|
|
687
|
+
createSyncIdCounter,
|
|
688
|
+
createSyncedDb,
|
|
689
|
+
createTypedEventTarget,
|
|
690
|
+
createWorkerDbClient,
|
|
691
|
+
deserializeHLC,
|
|
692
|
+
dummyKysely,
|
|
693
|
+
ensureSingletonExecution,
|
|
694
|
+
generateId,
|
|
695
|
+
initializeWorkerDb,
|
|
696
|
+
introspectDb,
|
|
697
|
+
isWorkerInitMessage,
|
|
698
|
+
isWorkerInitResponse,
|
|
699
|
+
isWorkerNotificationMessage,
|
|
700
|
+
isWorkerRequestMessage,
|
|
701
|
+
isWorkerResponseMessage,
|
|
702
|
+
jsonSafeParse,
|
|
703
|
+
makeCrdtTable,
|
|
704
|
+
orderBy,
|
|
705
|
+
registerCrdtFunctions,
|
|
706
|
+
serializeHLC,
|
|
707
|
+
startPerformanceLogger,
|
|
708
|
+
syncDbWorkerLockName
|
|
709
|
+
};
|
|
710
|
+
//# sourceMappingURL=index.js.map
|