@sqlite-sync/core 0.0.1 → 0.0.2
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-627DSM2Q.js +1410 -0
- package/dist/chunk-627DSM2Q.js.map +1 -0
- package/dist/chunk-UGF5IU53.js +132 -0
- package/dist/chunk-UGF5IU53.js.map +1 -0
- package/dist/crdt-schema-DQ1cYsFE.d.ts +63 -0
- package/dist/crdt-sync-remote-source-idoIjMcs.d.ts +479 -0
- package/dist/index.d.ts +117 -207
- package/dist/index.js +614 -357
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +50 -26
- package/dist/server.js +28 -36
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +13 -11
- package/dist/worker.js +275 -140
- package/dist/worker.js.map +1 -1
- package/package.json +11 -5
- package/dist/chunk-LK5FJCUD.js +0 -522
- package/dist/chunk-LK5FJCUD.js.map +0 -1
- package/dist/chunk-YLXMST5Z.js +0 -490
- package/dist/chunk-YLXMST5Z.js.map +0 -1
- package/dist/crdt-sync-producer-0toEpGf0.d.ts +0 -15
- package/dist/crdt-sync-remote-source-rrqinqLn.d.ts +0 -271
package/dist/index.js
CHANGED
|
@@ -1,206 +1,98 @@
|
|
|
1
1
|
import {
|
|
2
|
+
HLCCounter,
|
|
2
3
|
SQLiteDbWrapper,
|
|
3
|
-
|
|
4
|
+
applyKyselyEventsBatchFilters,
|
|
4
5
|
applyMemoryDbSchema,
|
|
5
6
|
applyWorkerDbSchema,
|
|
6
|
-
|
|
7
|
+
baseSystemMigrations,
|
|
8
|
+
compareHLC,
|
|
7
9
|
createBroadcastChannels,
|
|
10
|
+
createCrdtApplyFunction,
|
|
11
|
+
createCrdtStorage,
|
|
12
|
+
createCrdtSyncProducer,
|
|
8
13
|
createCrdtSyncRemoteSource,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
createKvStoreTableQuery,
|
|
15
|
+
createMigrations,
|
|
16
|
+
createMigrator,
|
|
17
|
+
createSQLiteCrdtApplyFunction,
|
|
18
|
+
createSQLiteKvStore,
|
|
19
|
+
createStoredValue,
|
|
20
|
+
deserializeHLC,
|
|
21
|
+
dummyKysely,
|
|
12
22
|
introspectDb,
|
|
13
|
-
|
|
14
|
-
isWorkerInitResponse,
|
|
23
|
+
isWorkerErrorResponseMessage,
|
|
15
24
|
isWorkerNotificationMessage,
|
|
16
|
-
isWorkerRequestMessage,
|
|
17
25
|
isWorkerResponseMessage,
|
|
26
|
+
runSystemMigrations,
|
|
27
|
+
serializeHLC,
|
|
18
28
|
startPerformanceLogger,
|
|
19
|
-
|
|
20
|
-
} from "./chunk-
|
|
29
|
+
syncDbClientLockName
|
|
30
|
+
} from "./chunk-627DSM2Q.js";
|
|
21
31
|
import {
|
|
22
32
|
TypedBroadcastChannel,
|
|
23
33
|
TypedEvent,
|
|
24
|
-
applyCrdtEventMutations,
|
|
25
|
-
crdtSchema,
|
|
26
|
-
createAsyncAutoFlushBuffer,
|
|
27
|
-
createAutoFlushBuffer,
|
|
28
|
-
createCrdtStorage,
|
|
29
|
-
createCrdtSyncProducer,
|
|
30
34
|
createDeferredPromise,
|
|
31
|
-
createSyncIdCounter,
|
|
32
35
|
createTypedEventTarget,
|
|
33
|
-
dummyKysely,
|
|
34
|
-
ensureSingletonExecution,
|
|
35
36
|
generateId,
|
|
36
37
|
jsonSafeParse,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
quoteId,
|
|
39
|
+
tryCatch,
|
|
40
|
+
tryCatchAsync
|
|
41
|
+
} from "./chunk-UGF5IU53.js";
|
|
40
42
|
|
|
41
|
-
// src/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
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();
|
|
43
|
+
// src/memory-db/sqlite-reactive-db.ts
|
|
44
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
45
|
+
|
|
46
|
+
// src/bound-map.ts
|
|
47
|
+
var BoundMap = class {
|
|
48
|
+
map = /* @__PURE__ */ new Map();
|
|
49
|
+
maxSize;
|
|
50
|
+
onRemove;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.maxSize = opts.maxSize;
|
|
53
|
+
this.onRemove = opts.onRemove;
|
|
69
54
|
}
|
|
70
|
-
|
|
71
|
-
if (this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
55
|
+
set = (key, value) => {
|
|
56
|
+
if (this.onRemove && this.map.has(key)) {
|
|
57
|
+
const old = this.map.get(key);
|
|
58
|
+
this.map.set(key, value);
|
|
59
|
+
this.onRemove(key, old);
|
|
75
60
|
} else {
|
|
76
|
-
this.
|
|
77
|
-
this.counter = hlc.counter + 1;
|
|
61
|
+
this.map.set(key, value);
|
|
78
62
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|
|
63
|
+
if (this.map.size > this.maxSize) {
|
|
64
|
+
const firstKey = this.map.keys().next().value;
|
|
65
|
+
this.delete(firstKey);
|
|
99
66
|
}
|
|
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
67
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!promise) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
promise.resolve(message.data);
|
|
130
|
-
workerRequestsMap.delete(message.requestId);
|
|
68
|
+
get = (key) => {
|
|
69
|
+
return this.map.get(key);
|
|
131
70
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
71
|
+
delete = (key) => {
|
|
72
|
+
if (this.onRemove && this.map.has(key)) {
|
|
73
|
+
const value = this.map.get(key);
|
|
74
|
+
this.map.delete(key);
|
|
75
|
+
this.onRemove(key, value);
|
|
76
|
+
} else {
|
|
77
|
+
this.map.delete(key);
|
|
138
78
|
}
|
|
139
79
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
80
|
+
clear = () => {
|
|
81
|
+
const onRemove = this.onRemove;
|
|
82
|
+
if (onRemove) {
|
|
83
|
+
this.map.forEach((value, key) => {
|
|
84
|
+
onRemove(key, value);
|
|
85
|
+
});
|
|
163
86
|
}
|
|
164
|
-
|
|
165
|
-
worker.onmessage = null;
|
|
166
|
-
};
|
|
167
|
-
const configMessage = {
|
|
168
|
-
type: "init",
|
|
169
|
-
config
|
|
87
|
+
this.map.clear();
|
|
170
88
|
};
|
|
171
|
-
|
|
172
|
-
broadcastChannels.requests.postMessage({
|
|
173
|
-
type: "request",
|
|
174
|
-
requestId: crypto.randomUUID(),
|
|
175
|
-
method: "postInitReady",
|
|
176
|
-
args: []
|
|
177
|
-
});
|
|
178
|
-
return promise.promise;
|
|
179
|
-
}
|
|
89
|
+
};
|
|
180
90
|
|
|
181
91
|
// src/memory-db/sqlite-reactive-db.ts
|
|
182
|
-
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
183
92
|
var sqliteModule = null;
|
|
184
93
|
function createSQLiteReactiveDb(opts) {
|
|
185
94
|
return SQLiteReactiveDb.create(opts);
|
|
186
95
|
}
|
|
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
96
|
var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
205
97
|
db;
|
|
206
98
|
sqlite3;
|
|
@@ -211,14 +103,14 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
211
103
|
this.sqlite3 = sqlite3;
|
|
212
104
|
this.logger = logger;
|
|
213
105
|
this.db = new SQLiteDbWrapper({
|
|
214
|
-
db: new sqlite3.oo1.DB({ filename: ":memory:" }),
|
|
106
|
+
db: () => new sqlite3.oo1.DB({ filename: ":memory:" }),
|
|
215
107
|
logger: this.logger,
|
|
216
108
|
loggerPrefix: "memory",
|
|
217
109
|
sqlite3
|
|
218
110
|
});
|
|
219
111
|
}
|
|
220
112
|
static async create(opts) {
|
|
221
|
-
const logger = opts.logger
|
|
113
|
+
const logger = opts.logger;
|
|
222
114
|
const perf = startPerformanceLogger(logger);
|
|
223
115
|
if (!sqliteModule) {
|
|
224
116
|
sqliteModule = await sqlite3InitModule();
|
|
@@ -228,24 +120,38 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
228
120
|
db.useSnapshot(opts.snapshot);
|
|
229
121
|
}
|
|
230
122
|
db.registerDbHooks();
|
|
231
|
-
perf.logEnd("createSQLiteMemoryDb", "success", "
|
|
123
|
+
perf.logEnd("createSQLiteMemoryDb", "success", "system");
|
|
232
124
|
return db;
|
|
233
125
|
}
|
|
126
|
+
liveQueryStatements = new BoundMap({
|
|
127
|
+
maxSize: 100,
|
|
128
|
+
onRemove(_, value) {
|
|
129
|
+
value.finalize();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
234
132
|
createLiveQuery(query) {
|
|
235
|
-
const fetchRows = () =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
133
|
+
const fetchRows = (parameters) => {
|
|
134
|
+
let statement = this.liveQueryStatements.get(query.sql);
|
|
135
|
+
if (!statement) {
|
|
136
|
+
statement = this.db.prepare(query.sql);
|
|
137
|
+
this.liveQueryStatements.set(query.sql, statement);
|
|
138
|
+
}
|
|
139
|
+
return statement.execute(parameters);
|
|
140
|
+
};
|
|
239
141
|
let rows = null;
|
|
240
142
|
const getRows = () => {
|
|
241
143
|
if (!rows) {
|
|
242
|
-
rows = fetchRows();
|
|
144
|
+
rows = fetchRows(query.parameters);
|
|
243
145
|
}
|
|
244
146
|
return rows;
|
|
245
147
|
};
|
|
246
148
|
let subscriber = null;
|
|
247
|
-
|
|
248
|
-
|
|
149
|
+
let lastParameters = query.parameters;
|
|
150
|
+
const refresh = (parameters) => {
|
|
151
|
+
if (parameters) {
|
|
152
|
+
lastParameters = parameters;
|
|
153
|
+
}
|
|
154
|
+
rows = fetchRows(lastParameters);
|
|
249
155
|
subscriber?.();
|
|
250
156
|
};
|
|
251
157
|
const subscribe = (onchange) => {
|
|
@@ -265,16 +171,14 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
265
171
|
return { getRows, refresh, subscribe };
|
|
266
172
|
}
|
|
267
173
|
subscribeToQueryChanges(params) {
|
|
268
|
-
const { sql
|
|
269
|
-
const tables = this.getTablesUsed(
|
|
174
|
+
const { sql, onDataChange } = params;
|
|
175
|
+
const tables = this.getTablesUsed(sql);
|
|
270
176
|
const readTables = /* @__PURE__ */ new Set();
|
|
271
177
|
for (const table of tables) {
|
|
272
178
|
if (!readTables.has(table.name)) {
|
|
273
179
|
readTables.add(table.name);
|
|
274
180
|
} else if (table.isWrite) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
"This query writes and reads from the same table. This may cause infinite loops."
|
|
277
|
-
);
|
|
181
|
+
throw new Error("This query writes and reads from the same table. This may cause infinite loops.");
|
|
278
182
|
}
|
|
279
183
|
}
|
|
280
184
|
const notifyDataChange = createDebouncedCallback(() => {
|
|
@@ -287,15 +191,9 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
287
191
|
return {
|
|
288
192
|
unsubscribe: () => {
|
|
289
193
|
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
|
-
);
|
|
194
|
+
this.eventTarget.removeEventListener(`table:${table}`, notifyDataChange);
|
|
298
195
|
}
|
|
196
|
+
this.eventTarget.removeEventListener("any-table-changed", notifyDataChange);
|
|
299
197
|
}
|
|
300
198
|
};
|
|
301
199
|
}
|
|
@@ -312,17 +210,18 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
312
210
|
getTablesUsed(query) {
|
|
313
211
|
if (!this.tablesUsedStatement) {
|
|
314
212
|
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'"
|
|
213
|
+
"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'",
|
|
214
|
+
{ loggerLevel: "system" }
|
|
316
215
|
);
|
|
317
216
|
}
|
|
318
217
|
const tables = this.tablesUsedStatement.execute([query]);
|
|
319
|
-
if (tables.length
|
|
218
|
+
if (tables.length === 0 && query.toLowerCase().includes("delete")) {
|
|
320
219
|
tables.push(...this.getClearedTables(query));
|
|
321
220
|
}
|
|
322
221
|
return tables;
|
|
323
222
|
}
|
|
324
223
|
getClearedTables(query) {
|
|
325
|
-
const operations = this.db.execute(`EXPLAIN ${query.split(";")[0]}
|
|
224
|
+
const operations = this.db.execute(`EXPLAIN ${query.split(";")[0]}`, { loggerLevel: "system" }).rows;
|
|
326
225
|
const clearedTablesRootPages = /* @__PURE__ */ new Set();
|
|
327
226
|
for (const operation of operations) {
|
|
328
227
|
if (operation.opcode === "Clear" && operation.p2 === 0) {
|
|
@@ -335,7 +234,8 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
335
234
|
const tableNames = this.db.execute(
|
|
336
235
|
`select t.tbl_name as name, true as isWrite from sqlite_master as t where t.rootpage in (${Array.from(
|
|
337
236
|
clearedTablesRootPages
|
|
338
|
-
).join(",")})
|
|
237
|
+
).join(",")})`,
|
|
238
|
+
{ loggerLevel: "system" }
|
|
339
239
|
).rows;
|
|
340
240
|
return tableNames;
|
|
341
241
|
}
|
|
@@ -395,17 +295,21 @@ var SQLiteReactiveDb = class _SQLiteReactiveDb {
|
|
|
395
295
|
createSnapshot() {
|
|
396
296
|
const perf = startPerformanceLogger(this.logger);
|
|
397
297
|
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
|
-
);
|
|
298
|
+
perf.logEnd("createSnapshot", `snapshot size: ${snapshot.byteLength}`, "info");
|
|
403
299
|
return snapshot;
|
|
404
300
|
}
|
|
405
301
|
useSnapshot(snapshot) {
|
|
406
302
|
this.db.useSnapshot(snapshot);
|
|
407
303
|
this.notifyTableSubscribers();
|
|
408
304
|
}
|
|
305
|
+
dispose() {
|
|
306
|
+
this.liveQueryStatements.clear();
|
|
307
|
+
if (this.tablesUsedStatement) {
|
|
308
|
+
this.tablesUsedStatement.finalize();
|
|
309
|
+
this.tablesUsedStatement = null;
|
|
310
|
+
}
|
|
311
|
+
this.db.close();
|
|
312
|
+
}
|
|
409
313
|
};
|
|
410
314
|
function createDebouncedCallback(callback, delay) {
|
|
411
315
|
let timeout = null;
|
|
@@ -428,8 +332,89 @@ function createDebouncedCallback(callback, delay) {
|
|
|
428
332
|
};
|
|
429
333
|
}
|
|
430
334
|
|
|
431
|
-
// src/
|
|
432
|
-
|
|
335
|
+
// src/sqlite-crdt/crdt-schema.ts
|
|
336
|
+
function createSyncDbSchema({ migrations }) {
|
|
337
|
+
return new CrdtSchemaBuilder({ tables: [], migrations });
|
|
338
|
+
}
|
|
339
|
+
var CrdtSchemaBuilder = class _CrdtSchemaBuilder {
|
|
340
|
+
constructor(config) {
|
|
341
|
+
this.config = config;
|
|
342
|
+
}
|
|
343
|
+
get tablesConfig() {
|
|
344
|
+
return this.config.tables;
|
|
345
|
+
}
|
|
346
|
+
get migrations() {
|
|
347
|
+
return this.config.migrations;
|
|
348
|
+
}
|
|
349
|
+
get "~clientSchema"() {
|
|
350
|
+
console.warn("~clientSchema should not be accessed on the client");
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
get "~serverSchema"() {
|
|
354
|
+
console.warn("~serverSchema should not be accessed on the server");
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
get "~mutationsSchema"() {
|
|
358
|
+
console.warn("~mutationsSchema should not be accessed on the client");
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
addTable() {
|
|
362
|
+
const withConfig = ({
|
|
363
|
+
baseTableName,
|
|
364
|
+
crdtTableName
|
|
365
|
+
}) => {
|
|
366
|
+
this.config.tables.push({ baseTableName, crdtTableName });
|
|
367
|
+
return new _CrdtSchemaBuilder(this.config);
|
|
368
|
+
};
|
|
369
|
+
return { withConfig };
|
|
370
|
+
}
|
|
371
|
+
build() {
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/sqlite-crdt/crdt-storage-mutator.ts
|
|
377
|
+
function createCrdtStorageMutator({ storage }) {
|
|
378
|
+
const mapToStorageEvent = (event) => {
|
|
379
|
+
switch (event.type) {
|
|
380
|
+
case "item-created":
|
|
381
|
+
return {
|
|
382
|
+
type: "item-created",
|
|
383
|
+
dataset: event.dataset,
|
|
384
|
+
item_id: event.item_id,
|
|
385
|
+
payload: JSON.stringify(event.payload)
|
|
386
|
+
};
|
|
387
|
+
case "item-updated":
|
|
388
|
+
return {
|
|
389
|
+
type: "item-updated",
|
|
390
|
+
dataset: event.dataset,
|
|
391
|
+
item_id: event.item_id,
|
|
392
|
+
payload: JSON.stringify(event.payload)
|
|
393
|
+
};
|
|
394
|
+
case "item-deleted":
|
|
395
|
+
return {
|
|
396
|
+
type: "item-updated",
|
|
397
|
+
dataset: event.dataset,
|
|
398
|
+
item_id: event.item_id,
|
|
399
|
+
payload: JSON.stringify({ tombstone: 1 })
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const enqueueEvents = (events) => {
|
|
404
|
+
storage.enqueueOwnEvents(events.map(mapToStorageEvent));
|
|
405
|
+
};
|
|
406
|
+
const createEvent = (event) => {
|
|
407
|
+
return event;
|
|
408
|
+
};
|
|
409
|
+
const enqueueEvent = (event) => {
|
|
410
|
+
storage.enqueueOwnEvents([mapToStorageEvent(event)]);
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
enqueueEvents,
|
|
414
|
+
createEvent,
|
|
415
|
+
enqueueEvent
|
|
416
|
+
};
|
|
417
|
+
}
|
|
433
418
|
|
|
434
419
|
// src/sqlite-crdt/make-crdt-table.ts
|
|
435
420
|
function makeCrdtTable({
|
|
@@ -441,23 +426,54 @@ function makeCrdtTable({
|
|
|
441
426
|
if (!tableSchema) {
|
|
442
427
|
throw new Error(`Table ${baseTableName} not found`);
|
|
443
428
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
429
|
+
const columns = new Map(tableSchema.columns.map((c) => [c.name, c]));
|
|
430
|
+
const idColumn = columns.get("id");
|
|
431
|
+
if (!idColumn) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Table "${baseTableName}" is missing a required "id" column. CRDT tables must have an "id" column to identify items.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (idColumn.dataType.toUpperCase() !== "TEXT") {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Table "${baseTableName}": "id" column must be of type TEXT, got "${idColumn.dataType}". CRDT item IDs are stored as strings.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const tombstoneColumn = columns.get("tombstone");
|
|
442
|
+
if (!tombstoneColumn) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Table "${baseTableName}" is missing a required "tombstone" column. CRDT tables must have a "tombstone" INTEGER column for soft deletes.`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const tombstoneType = tombstoneColumn.dataType.toUpperCase();
|
|
448
|
+
if (tombstoneType !== "INTEGER" && tombstoneType !== "BOOLEAN") {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Table "${baseTableName}": "tombstone" column must be of type INTEGER or BOOLEAN, got "${tombstoneColumn.dataType}". It is compared as 0/1 for soft deletes.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
db.execute(
|
|
454
|
+
`
|
|
455
|
+
create view ${quoteId(crdtTableName)} as
|
|
456
|
+
select * from ${quoteId(baseTableName)}
|
|
457
|
+
where tombstone = 0;`,
|
|
458
|
+
{ loggerLevel: "system" }
|
|
459
|
+
);
|
|
448
460
|
const allColumnNames = tableSchema.columns.map((column) => column.name);
|
|
449
|
-
const jsonPayload = (from) =>
|
|
450
|
-
db.execute(
|
|
451
|
-
|
|
452
|
-
|
|
461
|
+
const jsonPayload = (from) => `'{'||${allColumnNames.map((col) => `'"${col}":'||json_quote(${from}.${quoteId(col)})`).join("||','||")}||'}'`;
|
|
462
|
+
db.execute(
|
|
463
|
+
`
|
|
464
|
+
create trigger ${quoteId(`${crdtTableName}_created`)}
|
|
465
|
+
instead of insert on ${quoteId(crdtTableName)}
|
|
453
466
|
for each row
|
|
454
467
|
begin
|
|
455
468
|
select handle_item_created('${baseTableName}', ${jsonPayload("new")});
|
|
456
469
|
end;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
470
|
+
`,
|
|
471
|
+
{ loggerLevel: "system" }
|
|
472
|
+
);
|
|
473
|
+
db.execute(
|
|
474
|
+
`
|
|
475
|
+
create trigger ${quoteId(`${crdtTableName}_updated`)}
|
|
476
|
+
instead of update on ${quoteId(crdtTableName)}
|
|
461
477
|
for each row
|
|
462
478
|
begin
|
|
463
479
|
select handle_item_updated(
|
|
@@ -466,23 +482,133 @@ select handle_item_updated(
|
|
|
466
482
|
${jsonPayload("new")}
|
|
467
483
|
);
|
|
468
484
|
end;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
485
|
+
`,
|
|
486
|
+
{ loggerLevel: "system" }
|
|
487
|
+
);
|
|
488
|
+
db.execute(
|
|
489
|
+
`
|
|
490
|
+
create trigger ${quoteId(`${crdtTableName}_deleted`)}
|
|
491
|
+
instead of delete on ${quoteId(crdtTableName)}
|
|
473
492
|
for each row
|
|
474
493
|
when old.tombstone = 0
|
|
475
494
|
begin
|
|
476
495
|
select handle_item_deleted('${baseTableName}', old.id);
|
|
477
496
|
end;
|
|
478
|
-
|
|
497
|
+
`,
|
|
498
|
+
{ loggerLevel: "system" }
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
function registerCrdtFunctions({
|
|
502
|
+
reactiveDb,
|
|
503
|
+
storage
|
|
504
|
+
}) {
|
|
505
|
+
let eventApplied = false;
|
|
506
|
+
reactiveDb.db.createScalarFunction({
|
|
507
|
+
name: "handle_item_created",
|
|
508
|
+
deterministic: false,
|
|
509
|
+
directOnly: false,
|
|
510
|
+
innocuous: false,
|
|
511
|
+
callback: (dataset, payloadRaw) => {
|
|
512
|
+
const payload = JSON.parse(payloadRaw);
|
|
513
|
+
storage.applyOwnEvent(
|
|
514
|
+
{
|
|
515
|
+
type: "item-created",
|
|
516
|
+
dataset,
|
|
517
|
+
item_id: payload.id,
|
|
518
|
+
payload: payloadRaw
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
wrapInTransaction: false
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
eventApplied = true;
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
reactiveDb.db.createScalarFunction({
|
|
529
|
+
name: "handle_item_updated",
|
|
530
|
+
deterministic: false,
|
|
531
|
+
directOnly: false,
|
|
532
|
+
innocuous: false,
|
|
533
|
+
callback: (dataset, oldPayloadRaw, newPayloadRaw) => {
|
|
534
|
+
const tableSchema = reactiveDb.db.dbSchema[dataset];
|
|
535
|
+
const oldPayload = JSON.parse(oldPayloadRaw);
|
|
536
|
+
const newPayload = JSON.parse(newPayloadRaw);
|
|
537
|
+
let hasDiff = false;
|
|
538
|
+
const updatePayload = {};
|
|
539
|
+
for (const column of tableSchema.columns) {
|
|
540
|
+
const oldValue = oldPayload[column.name];
|
|
541
|
+
const newValue = newPayload[column.name];
|
|
542
|
+
if (oldValue === newValue) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
hasDiff = true;
|
|
546
|
+
updatePayload[column.name] = newValue;
|
|
547
|
+
}
|
|
548
|
+
if (!hasDiff) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
storage.applyOwnEvent(
|
|
552
|
+
{
|
|
553
|
+
type: "item-updated",
|
|
554
|
+
dataset,
|
|
555
|
+
item_id: oldPayload.id,
|
|
556
|
+
payload: JSON.stringify(updatePayload)
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
wrapInTransaction: false
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
eventApplied = true;
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
reactiveDb.db.createScalarFunction({
|
|
567
|
+
name: "handle_item_deleted",
|
|
568
|
+
deterministic: false,
|
|
569
|
+
directOnly: false,
|
|
570
|
+
innocuous: false,
|
|
571
|
+
callback: (dataset, itemId) => {
|
|
572
|
+
storage.applyOwnEvent(
|
|
573
|
+
{
|
|
574
|
+
type: "item-updated",
|
|
575
|
+
dataset,
|
|
576
|
+
item_id: itemId,
|
|
577
|
+
payload: JSON.stringify({ tombstone: 1 })
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
wrapInTransaction: false
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
eventApplied = true;
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
reactiveDb.addEventListener("transaction-committed", () => {
|
|
588
|
+
if (eventApplied) {
|
|
589
|
+
eventApplied = false;
|
|
590
|
+
storage.dispatchEventsApplied();
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
reactiveDb.addEventListener("transaction-rolled-back", () => {
|
|
594
|
+
eventApplied = false;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/db-id.ts
|
|
599
|
+
var dbIdRegex = /^[a-zA-Z][a-zA-Z\-0-9]{2,63}$/;
|
|
600
|
+
function validateDbId(dbId) {
|
|
601
|
+
if (!dbIdRegex.test(dbId)) {
|
|
602
|
+
throw new Error("Invalid dbId. Must be between 3 and 64 characters long and start with a letter.");
|
|
603
|
+
}
|
|
479
604
|
}
|
|
480
605
|
|
|
481
606
|
// src/memory-db/memory-db.ts
|
|
482
607
|
async function createMemoryDb({
|
|
608
|
+
nodeId,
|
|
609
|
+
migrator,
|
|
483
610
|
reactiveDb: _reactiveDb,
|
|
484
611
|
hlcCounter,
|
|
485
|
-
tabId,
|
|
486
612
|
crdtTables
|
|
487
613
|
}) {
|
|
488
614
|
const reactiveDb = _reactiveDb;
|
|
@@ -495,216 +621,347 @@ async function createMemoryDb({
|
|
|
495
621
|
crdtTableName: table.crdtTableName
|
|
496
622
|
});
|
|
497
623
|
}
|
|
498
|
-
const localSyncId =
|
|
499
|
-
|
|
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
|
-
});
|
|
624
|
+
const localSyncId = createStoredValue({
|
|
625
|
+
initialValue: 0
|
|
536
626
|
});
|
|
537
627
|
const crdtStorage = createCrdtStorage({
|
|
628
|
+
nodeId,
|
|
538
629
|
syncId: localSyncId,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
630
|
+
hlc: hlcCounter,
|
|
631
|
+
persistEvent: (event) => persistEvent(db, event),
|
|
632
|
+
getEventsBatch: (opts) => getEventsBatch(db, opts),
|
|
633
|
+
migrator,
|
|
634
|
+
handleCrdtEventApply: createSQLiteCrdtApplyFunction({
|
|
542
635
|
db,
|
|
543
|
-
event,
|
|
544
636
|
updateLogTableName: "crdt_update_log"
|
|
545
637
|
}),
|
|
546
|
-
|
|
638
|
+
updateEvent: (syncId, update) => updateEvent(db, syncId, update),
|
|
639
|
+
transaction: (callback) => db.executeTransaction(callback)
|
|
640
|
+
});
|
|
641
|
+
registerCrdtFunctions({
|
|
642
|
+
reactiveDb,
|
|
643
|
+
storage: crdtStorage
|
|
547
644
|
});
|
|
548
645
|
return {
|
|
549
646
|
crdtStorage
|
|
550
647
|
};
|
|
551
648
|
}
|
|
552
|
-
function
|
|
649
|
+
function persistEvent(db, event) {
|
|
553
650
|
db.executePrepared(
|
|
554
|
-
"
|
|
651
|
+
"persist-crdt-event",
|
|
555
652
|
event,
|
|
556
653
|
(db2, params) => db2.insertInto("persisted_crdt_events").values({
|
|
557
|
-
status: params("status"),
|
|
558
|
-
sync_id: params("sync_id"),
|
|
559
654
|
type: params("type"),
|
|
560
|
-
timestamp: params("timestamp"),
|
|
561
655
|
dataset: params("dataset"),
|
|
562
656
|
item_id: params("item_id"),
|
|
563
657
|
payload: params("payload"),
|
|
564
|
-
|
|
565
|
-
|
|
658
|
+
schema_version: params("schema_version"),
|
|
659
|
+
sync_id: params("sync_id"),
|
|
660
|
+
status: params("status"),
|
|
661
|
+
timestamp: params("timestamp"),
|
|
662
|
+
origin: params("origin"),
|
|
663
|
+
source_node_id: params("source_node_id")
|
|
664
|
+
}),
|
|
665
|
+
{ loggerLevel: "system" }
|
|
566
666
|
);
|
|
567
667
|
}
|
|
568
|
-
function
|
|
569
|
-
db.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
});
|
|
668
|
+
function getEventsBatch(db, opts) {
|
|
669
|
+
return db.executeKysely(
|
|
670
|
+
(db2) => applyKyselyEventsBatchFilters(db2.selectFrom("persisted_crdt_events").selectAll(), {
|
|
671
|
+
limit: 50,
|
|
672
|
+
...opts
|
|
673
|
+
}),
|
|
674
|
+
{ loggerLevel: "system" }
|
|
675
|
+
).rows;
|
|
578
676
|
}
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
"
|
|
582
|
-
{
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
677
|
+
function updateEvent(db, syncId, update) {
|
|
678
|
+
db.executePrepared(
|
|
679
|
+
"update-crdt-event",
|
|
680
|
+
{ syncId, ...update },
|
|
681
|
+
(db2, params) => db2.updateTable("persisted_crdt_events").set({
|
|
682
|
+
status: params("status"),
|
|
683
|
+
schema_version: params("schema_version"),
|
|
684
|
+
type: params("type"),
|
|
685
|
+
dataset: params("dataset"),
|
|
686
|
+
item_id: params("item_id"),
|
|
687
|
+
payload: params("payload")
|
|
688
|
+
}).where("sync_id", "=", params("syncId")),
|
|
689
|
+
{ loggerLevel: "system" }
|
|
586
690
|
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/worker-db/db-worker-client.ts
|
|
694
|
+
var createWorkerDbClient = async ({
|
|
695
|
+
broadcastChannels,
|
|
696
|
+
worker,
|
|
697
|
+
config
|
|
698
|
+
}) => {
|
|
699
|
+
const eventTarget = createTypedEventTarget();
|
|
700
|
+
const workerRequestsMap = /* @__PURE__ */ new Map();
|
|
701
|
+
let isDisposed = false;
|
|
702
|
+
const queryWorker = (method, args) => {
|
|
703
|
+
if (isDisposed) {
|
|
704
|
+
return Promise.reject(new Error("Worker client disposed"));
|
|
705
|
+
}
|
|
706
|
+
const requestId = crypto.randomUUID();
|
|
707
|
+
const promise = createDeferredPromise({
|
|
708
|
+
timeout: 3e4,
|
|
709
|
+
onTimeout: () => workerRequestsMap.delete(requestId)
|
|
710
|
+
});
|
|
711
|
+
workerRequestsMap.set(requestId, promise);
|
|
712
|
+
const request = {
|
|
713
|
+
type: "request",
|
|
714
|
+
requestId,
|
|
715
|
+
method,
|
|
716
|
+
args
|
|
717
|
+
};
|
|
718
|
+
broadcastChannels.requests.postMessage(request);
|
|
719
|
+
return promise.promise;
|
|
720
|
+
};
|
|
721
|
+
const handleWorkerResponse = (message) => {
|
|
722
|
+
const promise = workerRequestsMap.get(message.requestId);
|
|
723
|
+
if (!promise) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
promise.resolve(message.data);
|
|
727
|
+
workerRequestsMap.delete(message.requestId);
|
|
728
|
+
};
|
|
729
|
+
const handleWorkerError = (message) => {
|
|
730
|
+
const promise = workerRequestsMap.get(message.requestId);
|
|
731
|
+
if (!promise) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
promise.reject(new Error(message.error));
|
|
735
|
+
workerRequestsMap.delete(message.requestId);
|
|
736
|
+
};
|
|
737
|
+
broadcastChannels.responses.onmessage = (event) => {
|
|
738
|
+
const message = event.data;
|
|
739
|
+
if (isWorkerResponseMessage(message)) {
|
|
740
|
+
handleWorkerResponse(message);
|
|
741
|
+
} else if (isWorkerErrorResponseMessage(message)) {
|
|
742
|
+
handleWorkerError(message);
|
|
743
|
+
} else if (isWorkerNotificationMessage(message)) {
|
|
744
|
+
eventTarget.dispatchEvent(message.notificationType, message);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
const rpc = {
|
|
748
|
+
execute: (query) => queryWorker("execute", [query]),
|
|
749
|
+
getSnapshot: () => queryWorker("getSnapshot", []),
|
|
750
|
+
pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
|
|
751
|
+
pullEvents: (params) => queryWorker("pullEvents", [params]),
|
|
752
|
+
postState: () => queryWorker("postState", []),
|
|
753
|
+
goOnline: () => queryWorker("goOnline", []),
|
|
754
|
+
goOffline: () => queryWorker("goOffline", [])
|
|
755
|
+
};
|
|
756
|
+
const statePromise = awaitWorkerState(eventTarget);
|
|
757
|
+
postWorkerConfig(worker, config);
|
|
758
|
+
rpc.postState().catch(() => {
|
|
759
|
+
});
|
|
760
|
+
let workerState = await statePromise;
|
|
761
|
+
eventTarget.addEventListener("state-changed", (event) => {
|
|
762
|
+
workerState = event.payload.state;
|
|
763
|
+
});
|
|
764
|
+
const dispose = () => {
|
|
765
|
+
isDisposed = true;
|
|
766
|
+
broadcastChannels.responses.onmessage = null;
|
|
767
|
+
for (const [id, deferred] of workerRequestsMap) {
|
|
768
|
+
deferred.reject(new Error("Worker client disposed"));
|
|
769
|
+
workerRequestsMap.delete(id);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
587
772
|
return {
|
|
588
|
-
|
|
589
|
-
|
|
773
|
+
...rpc,
|
|
774
|
+
addEventListener: eventTarget.addEventListener,
|
|
775
|
+
removeEventListener: eventTarget.removeEventListener,
|
|
776
|
+
getState: () => workerState,
|
|
777
|
+
dispose
|
|
590
778
|
};
|
|
779
|
+
};
|
|
780
|
+
function awaitWorkerState(eventTarget) {
|
|
781
|
+
const promise = createDeferredPromise({ timeout: 15e3 });
|
|
782
|
+
const onStateChanged = (event) => {
|
|
783
|
+
promise.resolve(event.payload.state);
|
|
784
|
+
eventTarget.removeEventListener("state-changed", onStateChanged);
|
|
785
|
+
};
|
|
786
|
+
eventTarget.addEventListener("state-changed", onStateChanged);
|
|
787
|
+
return promise.promise;
|
|
591
788
|
}
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
"
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
);
|
|
789
|
+
function postWorkerConfig(worker, config) {
|
|
790
|
+
const configMessage = {
|
|
791
|
+
type: "init",
|
|
792
|
+
config
|
|
793
|
+
};
|
|
794
|
+
worker.postMessage(configMessage);
|
|
598
795
|
}
|
|
599
796
|
|
|
600
797
|
// src/sync-db.ts
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
798
|
+
var defaultLogger = (type, message, level = "info") => {
|
|
799
|
+
const logMessage = `[${type}] ${message}`;
|
|
800
|
+
switch (level) {
|
|
801
|
+
case "info":
|
|
802
|
+
console.log(logMessage);
|
|
803
|
+
break;
|
|
804
|
+
case "warning":
|
|
805
|
+
console.warn(logMessage);
|
|
806
|
+
break;
|
|
807
|
+
case "error":
|
|
808
|
+
console.error(logMessage);
|
|
809
|
+
break;
|
|
810
|
+
case "trace":
|
|
811
|
+
console.trace(logMessage);
|
|
812
|
+
break;
|
|
604
813
|
}
|
|
814
|
+
};
|
|
815
|
+
async function createSyncedDb(options) {
|
|
816
|
+
validateDbId(options.dbId);
|
|
817
|
+
const perf = startPerformanceLogger(defaultLogger);
|
|
605
818
|
const tabId = generateId();
|
|
606
|
-
const broadcastChannels = createBroadcastChannels();
|
|
607
|
-
|
|
819
|
+
const broadcastChannels = createBroadcastChannels(options.dbId);
|
|
820
|
+
const clientLockAcquired = createDeferredPromise();
|
|
821
|
+
const clientLockRelease = createDeferredPromise();
|
|
822
|
+
navigator.locks.request(`${syncDbClientLockName}-${options.dbId}`, { mode: "shared" }, () => {
|
|
823
|
+
clientLockAcquired.resolve();
|
|
824
|
+
return clientLockRelease.promise;
|
|
825
|
+
});
|
|
826
|
+
await clientLockAcquired.promise;
|
|
827
|
+
const workerClient = await createWorkerDbClient({
|
|
608
828
|
worker: options.worker,
|
|
609
|
-
broadcastChannels,
|
|
610
829
|
config: {
|
|
611
830
|
clientId: generateId(),
|
|
612
|
-
|
|
831
|
+
dbId: options.dbId,
|
|
613
832
|
clearOnInit: options.clearOnInit,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
room: ""
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
const workerClient = createWorkerDbClient({
|
|
833
|
+
props: options.workerProps
|
|
834
|
+
},
|
|
621
835
|
broadcastChannels
|
|
622
836
|
});
|
|
623
837
|
const hlcCounter = new HLCCounter(tabId, () => Date.now());
|
|
624
838
|
const workerClientSnapshot = await workerClient.getSnapshot();
|
|
625
839
|
const reactiveDb = await createSQLiteReactiveDb({
|
|
626
|
-
snapshot: workerClientSnapshot.file
|
|
840
|
+
snapshot: workerClientSnapshot.file,
|
|
841
|
+
logger: defaultLogger
|
|
627
842
|
});
|
|
843
|
+
const memoryDbMigrator = {
|
|
844
|
+
currentSchemaVersion: workerClientSnapshot.schemaVersion,
|
|
845
|
+
latestSchemaVersion: workerClientSnapshot.schemaVersion,
|
|
846
|
+
migrateDbToLatest: () => {
|
|
847
|
+
throw new Error("Memory DB migrations are not implemented");
|
|
848
|
+
},
|
|
849
|
+
migrateEvent: (event, targetVersion) => {
|
|
850
|
+
if (event.schema_version === targetVersion) {
|
|
851
|
+
return event;
|
|
852
|
+
}
|
|
853
|
+
throw new Error("Memory DB migrations are not implemented");
|
|
854
|
+
},
|
|
855
|
+
migrateEvents: (events) => events
|
|
856
|
+
};
|
|
628
857
|
const { crdtStorage } = await createMemoryDb({
|
|
858
|
+
nodeId: tabId,
|
|
859
|
+
migrator: memoryDbMigrator,
|
|
629
860
|
reactiveDb,
|
|
630
861
|
hlcCounter,
|
|
631
|
-
|
|
632
|
-
crdtTables: options.crdtTables
|
|
862
|
+
crdtTables: options.syncDbSchema.tablesConfig
|
|
633
863
|
});
|
|
634
|
-
const
|
|
635
|
-
|
|
864
|
+
const pullSyncId = createStoredValue({
|
|
865
|
+
initialValue: workerClientSnapshot.syncId
|
|
636
866
|
});
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
867
|
+
const pushSyncId = createStoredValue({
|
|
868
|
+
initialValue: 0
|
|
869
|
+
});
|
|
870
|
+
const tabRemoteSource = createCrdtSyncRemoteSource({
|
|
871
|
+
bufferSize: 500,
|
|
872
|
+
pullSyncId,
|
|
873
|
+
pushSyncId,
|
|
640
874
|
storage: crdtStorage,
|
|
641
875
|
nodeId: tabId,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
876
|
+
migrator: memoryDbMigrator,
|
|
877
|
+
remoteFactory: ({ onEventsAvailable }) => {
|
|
878
|
+
const onNewEventChunkApplied = (event) => {
|
|
879
|
+
onEventsAvailable(event.payload.newSyncId);
|
|
880
|
+
};
|
|
881
|
+
workerClient.addEventListener("new-event-chunk-applied", onNewEventChunkApplied);
|
|
882
|
+
return {
|
|
883
|
+
pullEvents: (request) => workerClient.pullEvents(request),
|
|
884
|
+
pushEvents: (request) => workerClient.pushTabEvents(request),
|
|
885
|
+
disconnect: () => {
|
|
886
|
+
workerClient.removeEventListener("new-event-chunk-applied", onNewEventChunkApplied);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
654
889
|
}
|
|
655
890
|
});
|
|
891
|
+
tabRemoteSource.goOnline();
|
|
892
|
+
perf.logEnd("createSyncedDb", "initialized", "info");
|
|
893
|
+
let isDisposed = false;
|
|
894
|
+
const dispose = async () => {
|
|
895
|
+
if (isDisposed) return;
|
|
896
|
+
isDisposed = true;
|
|
897
|
+
clientLockRelease.resolve();
|
|
898
|
+
await tabRemoteSource.dispose();
|
|
899
|
+
broadcastChannels.requests.close();
|
|
900
|
+
broadcastChannels.responses.close();
|
|
901
|
+
workerClient.dispose();
|
|
902
|
+
reactiveDb.dispose();
|
|
903
|
+
};
|
|
656
904
|
return {
|
|
657
|
-
db:
|
|
658
|
-
|
|
659
|
-
|
|
905
|
+
db: {
|
|
906
|
+
execute: reactiveDb.db.execute.bind(reactiveDb.db),
|
|
907
|
+
executeKysely: reactiveDb.db.executeKysely.bind(reactiveDb.db),
|
|
908
|
+
executeTransaction: reactiveDb.db.executeTransaction.bind(reactiveDb.db),
|
|
909
|
+
createLiveQuery: reactiveDb.createLiveQuery.bind(reactiveDb)
|
|
910
|
+
},
|
|
911
|
+
state: {
|
|
912
|
+
getState: workerClient.getState.bind(workerClient),
|
|
913
|
+
subscribe: (onChange) => {
|
|
914
|
+
workerClient.addEventListener("state-changed", onChange);
|
|
915
|
+
return () => {
|
|
916
|
+
workerClient.removeEventListener("state-changed", onChange);
|
|
917
|
+
};
|
|
918
|
+
},
|
|
919
|
+
goOnline: workerClient.goOnline.bind(workerClient),
|
|
920
|
+
goOffline: workerClient.goOffline.bind(workerClient)
|
|
921
|
+
},
|
|
922
|
+
dispose,
|
|
923
|
+
_internal: {
|
|
924
|
+
executeAsync: workerClient.execute.bind(workerClient)
|
|
925
|
+
}
|
|
660
926
|
};
|
|
661
927
|
}
|
|
662
928
|
export {
|
|
663
929
|
HLCCounter,
|
|
664
930
|
SQLiteDbWrapper,
|
|
665
931
|
SQLiteReactiveDb,
|
|
666
|
-
SqliteDriver,
|
|
667
932
|
TypedBroadcastChannel,
|
|
668
933
|
TypedEvent,
|
|
669
|
-
|
|
934
|
+
applyKyselyEventsBatchFilters,
|
|
670
935
|
applyMemoryDbSchema,
|
|
671
936
|
applyWorkerDbSchema,
|
|
672
|
-
|
|
937
|
+
baseSystemMigrations,
|
|
673
938
|
compareHLC,
|
|
674
|
-
|
|
675
|
-
createAsyncAutoFlushBuffer,
|
|
676
|
-
createAutoFlushBuffer,
|
|
677
|
-
createBroadcastChannels,
|
|
939
|
+
createCrdtApplyFunction,
|
|
678
940
|
createCrdtStorage,
|
|
941
|
+
createCrdtStorageMutator,
|
|
679
942
|
createCrdtSyncProducer,
|
|
680
943
|
createCrdtSyncRemoteSource,
|
|
681
944
|
createDeferredPromise,
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
945
|
+
createKvStoreTableQuery,
|
|
946
|
+
createMigrations,
|
|
947
|
+
createMigrator,
|
|
948
|
+
createSQLiteCrdtApplyFunction,
|
|
949
|
+
createSQLiteKvStore,
|
|
950
|
+
createStoredValue,
|
|
951
|
+
createSyncDbSchema,
|
|
688
952
|
createSyncedDb,
|
|
689
953
|
createTypedEventTarget,
|
|
690
|
-
createWorkerDbClient,
|
|
691
954
|
deserializeHLC,
|
|
692
955
|
dummyKysely,
|
|
693
|
-
ensureSingletonExecution,
|
|
694
956
|
generateId,
|
|
695
|
-
initializeWorkerDb,
|
|
696
957
|
introspectDb,
|
|
697
|
-
isWorkerInitMessage,
|
|
698
|
-
isWorkerInitResponse,
|
|
699
|
-
isWorkerNotificationMessage,
|
|
700
|
-
isWorkerRequestMessage,
|
|
701
|
-
isWorkerResponseMessage,
|
|
702
958
|
jsonSafeParse,
|
|
703
959
|
makeCrdtTable,
|
|
704
|
-
|
|
705
|
-
|
|
960
|
+
quoteId,
|
|
961
|
+
runSystemMigrations,
|
|
706
962
|
serializeHLC,
|
|
707
963
|
startPerformanceLogger,
|
|
708
|
-
|
|
964
|
+
tryCatch,
|
|
965
|
+
tryCatchAsync
|
|
709
966
|
};
|
|
710
967
|
//# sourceMappingURL=index.js.map
|