@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
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TypedBroadcastChannel,
|
|
3
|
+
createTypedEventTarget,
|
|
4
|
+
ensureSingletonExecution,
|
|
5
|
+
quoteId,
|
|
6
|
+
tryCatchAsync
|
|
7
|
+
} from "./chunk-UGF5IU53.js";
|
|
8
|
+
|
|
9
|
+
// src/dummy-kysely.ts
|
|
10
|
+
import { DummyDriver, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
|
11
|
+
var dummyKysely = new Kysely({
|
|
12
|
+
dialect: {
|
|
13
|
+
createAdapter: () => new SqliteAdapter(),
|
|
14
|
+
createDriver: () => new DummyDriver(),
|
|
15
|
+
createQueryCompiler: () => new SqliteQueryCompiler(),
|
|
16
|
+
createIntrospector: (db) => new SqliteIntrospector(db)
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/hlc.ts
|
|
21
|
+
var MAX_COUNTER = 36 ** 5 - 1;
|
|
22
|
+
var DEFAULT_MAX_DRIFT_MS = 6 * 60 * 60 * 1e3;
|
|
23
|
+
var HLCCounter = class {
|
|
24
|
+
timestamp;
|
|
25
|
+
counter;
|
|
26
|
+
nodeId;
|
|
27
|
+
getTimestamp;
|
|
28
|
+
maxDrift;
|
|
29
|
+
constructor(nodeId, getTimestamp, maxDrift = DEFAULT_MAX_DRIFT_MS) {
|
|
30
|
+
this.timestamp = getTimestamp();
|
|
31
|
+
this.counter = 0;
|
|
32
|
+
this.nodeId = nodeId;
|
|
33
|
+
this.getTimestamp = getTimestamp;
|
|
34
|
+
this.maxDrift = maxDrift;
|
|
35
|
+
}
|
|
36
|
+
getCurrentHLC() {
|
|
37
|
+
return {
|
|
38
|
+
timestamp: this.timestamp,
|
|
39
|
+
counter: this.counter,
|
|
40
|
+
nodeId: this.nodeId
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
getNextHLC() {
|
|
44
|
+
const now = this.getTimestamp();
|
|
45
|
+
if (now > this.timestamp) {
|
|
46
|
+
this.timestamp = now;
|
|
47
|
+
this.counter = 0;
|
|
48
|
+
return this.getCurrentHLC();
|
|
49
|
+
}
|
|
50
|
+
this.counter++;
|
|
51
|
+
if (this.counter > MAX_COUNTER) {
|
|
52
|
+
throw new Error(`HLC counter overflow: exceeded max value ${MAX_COUNTER}`);
|
|
53
|
+
}
|
|
54
|
+
return this.getCurrentHLC();
|
|
55
|
+
}
|
|
56
|
+
mergeHLC(hlc) {
|
|
57
|
+
const now = this.getTimestamp();
|
|
58
|
+
if (hlc.timestamp - now > this.maxDrift) {
|
|
59
|
+
console.warn(
|
|
60
|
+
`HLC: ignoring far-future timestamp (remote=${hlc.timestamp}, local=${now}, drift=${hlc.timestamp - now}ms)`
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.timestamp === hlc.timestamp) {
|
|
65
|
+
this.counter = Math.max(this.counter, hlc.counter) + 1;
|
|
66
|
+
} else if (this.timestamp > hlc.timestamp) {
|
|
67
|
+
this.counter++;
|
|
68
|
+
} else {
|
|
69
|
+
this.timestamp = hlc.timestamp;
|
|
70
|
+
this.counter = hlc.counter + 1;
|
|
71
|
+
}
|
|
72
|
+
if (this.counter > MAX_COUNTER) {
|
|
73
|
+
throw new Error(`HLC counter overflow: exceeded max value ${MAX_COUNTER}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function serializeHLC(hlc) {
|
|
78
|
+
return `${hlc.timestamp.toString().padStart(15, "0")}:${hlc.counter.toString(36).padStart(5, "0")}:${hlc.nodeId}`;
|
|
79
|
+
}
|
|
80
|
+
function deserializeHLC(serialized) {
|
|
81
|
+
const parts = serialized.split(":");
|
|
82
|
+
if (parts.length < 3) {
|
|
83
|
+
throw new Error(`Invalid HLC format: expected at least 3 colon-separated segments, got ${parts.length}`);
|
|
84
|
+
}
|
|
85
|
+
const timestamp = parseInt(parts[0], 10);
|
|
86
|
+
const counter = parseInt(parts[1], 36);
|
|
87
|
+
if (Number.isNaN(timestamp) || Number.isNaN(counter)) {
|
|
88
|
+
throw new Error(`Invalid HLC values: timestamp=${parts[0]}, counter=${parts[1]}`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
timestamp,
|
|
92
|
+
counter,
|
|
93
|
+
nodeId: parts.slice(2).join(":")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function compareHLC(one, two) {
|
|
97
|
+
if (one.timestamp === two.timestamp) {
|
|
98
|
+
if (one.counter === two.counter) {
|
|
99
|
+
if (one.nodeId === two.nodeId) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
return one.nodeId < two.nodeId ? -1 : 1;
|
|
103
|
+
}
|
|
104
|
+
return one.counter - two.counter;
|
|
105
|
+
}
|
|
106
|
+
return one.timestamp - two.timestamp;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/introspection.ts
|
|
110
|
+
import { sql } from "kysely";
|
|
111
|
+
function tablesQuery(qb) {
|
|
112
|
+
return qb.selectFrom("sqlite_master").where("type", "in", ["table", "view"]).where("name", "not like", "sqlite_%").select(["name", "sql", "type"]).orderBy("name");
|
|
113
|
+
}
|
|
114
|
+
function introspectDb(_db) {
|
|
115
|
+
const db = _db;
|
|
116
|
+
const tables = db.executeKysely((db2) => tablesQuery(db2), {
|
|
117
|
+
loggerLevel: "system"
|
|
118
|
+
}).rows;
|
|
119
|
+
const tablesMetadata = db.executeKysely(
|
|
120
|
+
(db2) => db2.with("table_list", (qb) => tablesQuery(qb)).selectFrom(["table_list as tl", sql`pragma_table_info(tl.name)`.as("p")]).select(["tl.name as table", "p.cid", "p.name", "p.type", "p.notnull", "p.dflt_value", "p.pk"]).orderBy("tl.name").orderBy("p.cid"),
|
|
121
|
+
{ loggerLevel: "system" }
|
|
122
|
+
).rows;
|
|
123
|
+
const columnsByTable = {};
|
|
124
|
+
for (const row of tablesMetadata) {
|
|
125
|
+
columnsByTable[row.table] ??= [];
|
|
126
|
+
columnsByTable[row.table].push(row);
|
|
127
|
+
}
|
|
128
|
+
return Object.fromEntries(
|
|
129
|
+
tables.map(({ name, sql: sql2, type }) => {
|
|
130
|
+
let autoIncrementCol = sql2?.split(/[(),]/)?.find((it) => it.toLowerCase().includes("autoincrement"))?.trimStart()?.split(/\s+/)?.[0]?.replace(/["`]/g, "");
|
|
131
|
+
const columns = columnsByTable[name] ?? [];
|
|
132
|
+
if (!autoIncrementCol) {
|
|
133
|
+
const pkCols = columns.filter((r) => r.pk > 0);
|
|
134
|
+
if (pkCols.length === 1 && pkCols[0].type.toLowerCase() === "integer") {
|
|
135
|
+
autoIncrementCol = pkCols[0].name;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [
|
|
139
|
+
name,
|
|
140
|
+
{
|
|
141
|
+
name,
|
|
142
|
+
isView: type === "view",
|
|
143
|
+
columns: columns.map((col) => ({
|
|
144
|
+
name: col.name,
|
|
145
|
+
dataType: col.type,
|
|
146
|
+
isNullable: !col.notnull,
|
|
147
|
+
isAutoIncrementing: col.name === autoIncrementCol,
|
|
148
|
+
hasDefaultValue: col.dflt_value != null,
|
|
149
|
+
comment: void 0
|
|
150
|
+
}))
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/logger.ts
|
|
158
|
+
var startPerformanceLogger = (logger) => {
|
|
159
|
+
let startTime = performance.now();
|
|
160
|
+
return {
|
|
161
|
+
restart: () => {
|
|
162
|
+
startTime = performance.now();
|
|
163
|
+
},
|
|
164
|
+
logEnd: (type, message, level = "info") => {
|
|
165
|
+
const elapsed = performance.now() - startTime;
|
|
166
|
+
logger(type, `${elapsed.toFixed(2)}ms - ${message}`, level);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/sqlite-db-wrapper.ts
|
|
172
|
+
var SQLiteDbWrapper = class {
|
|
173
|
+
db = null;
|
|
174
|
+
sqlite3;
|
|
175
|
+
logger;
|
|
176
|
+
loggerPrefix;
|
|
177
|
+
loadedDbSchema = null;
|
|
178
|
+
dataPointers = [];
|
|
179
|
+
preparedStatements = [];
|
|
180
|
+
preparedStatementsMap = /* @__PURE__ */ new Map();
|
|
181
|
+
preparedRawStatementsMap = /* @__PURE__ */ new Map();
|
|
182
|
+
constructor(opts) {
|
|
183
|
+
this.db = opts.db();
|
|
184
|
+
this.sqlite3 = opts.sqlite3;
|
|
185
|
+
this.logger = opts.logger;
|
|
186
|
+
this.loggerPrefix = opts.loggerPrefix;
|
|
187
|
+
}
|
|
188
|
+
get ensureDb() {
|
|
189
|
+
if (!this.db) {
|
|
190
|
+
throw new Error("Database is already closed");
|
|
191
|
+
}
|
|
192
|
+
return this.db;
|
|
193
|
+
}
|
|
194
|
+
get dbSchema() {
|
|
195
|
+
if (!this.loadedDbSchema) {
|
|
196
|
+
this.loadedDbSchema = introspectDb(this);
|
|
197
|
+
}
|
|
198
|
+
return this.loadedDbSchema;
|
|
199
|
+
}
|
|
200
|
+
execute(opts, meta) {
|
|
201
|
+
const sql2 = typeof opts === "string" ? opts : opts.sql;
|
|
202
|
+
const bind = typeof opts === "string" ? void 0 : opts.parameters;
|
|
203
|
+
const perf = this.logger ? startPerformanceLogger(this.logger) : void 0;
|
|
204
|
+
const rows = this.ensureDb.exec({
|
|
205
|
+
sql: sql2,
|
|
206
|
+
bind,
|
|
207
|
+
returnValue: "resultRows",
|
|
208
|
+
rowMode: "object"
|
|
209
|
+
});
|
|
210
|
+
perf?.logEnd(`${this.loggerPrefix ?? ""}:query`, sql2, meta?.loggerLevel);
|
|
211
|
+
return { rows };
|
|
212
|
+
}
|
|
213
|
+
executeTransaction(callback) {
|
|
214
|
+
const transaction = this.beginTransaction();
|
|
215
|
+
try {
|
|
216
|
+
const result = callback(this);
|
|
217
|
+
transaction.commit();
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
transaction.rollback();
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
isInTransaction() {
|
|
225
|
+
return this.sqlite3.capi.sqlite3_get_autocommit(this.ensureDb) === 0;
|
|
226
|
+
}
|
|
227
|
+
beginTransaction() {
|
|
228
|
+
this.executePreparedRaw({
|
|
229
|
+
key: "$begin-transaction",
|
|
230
|
+
sql: "begin",
|
|
231
|
+
meta: {
|
|
232
|
+
loggerLevel: "system"
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
commit: () => {
|
|
237
|
+
this.executePreparedRaw({
|
|
238
|
+
key: "$commit-transaction",
|
|
239
|
+
sql: "commit",
|
|
240
|
+
meta: {
|
|
241
|
+
loggerLevel: "system"
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
rollback: () => {
|
|
246
|
+
this.executePreparedRaw({
|
|
247
|
+
key: "$rollback-transaction",
|
|
248
|
+
sql: "rollback",
|
|
249
|
+
meta: {
|
|
250
|
+
loggerLevel: "system"
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
prepare(sql2, opts) {
|
|
257
|
+
const perf = this.logger ? startPerformanceLogger(this.logger) : void 0;
|
|
258
|
+
const stmt = this.ensureDb.prepare(sql2);
|
|
259
|
+
perf?.logEnd(`${this.loggerPrefix ?? ""}:prepare`, sql2, opts?.loggerLevel);
|
|
260
|
+
let isFinalized = false;
|
|
261
|
+
const execute = (params) => {
|
|
262
|
+
if (isFinalized) {
|
|
263
|
+
throw new Error("Statement is finalized");
|
|
264
|
+
}
|
|
265
|
+
const perf2 = this.logger ? startPerformanceLogger(this.logger) : void 0;
|
|
266
|
+
if (params.length > 0) {
|
|
267
|
+
stmt.bind(params);
|
|
268
|
+
}
|
|
269
|
+
const results = [];
|
|
270
|
+
while (stmt.step()) {
|
|
271
|
+
results.push(stmt.get({}));
|
|
272
|
+
}
|
|
273
|
+
stmt.reset(true);
|
|
274
|
+
perf2?.logEnd(`${this.loggerPrefix ?? ""}:prepare-execute`, sql2, opts?.loggerLevel);
|
|
275
|
+
return results;
|
|
276
|
+
};
|
|
277
|
+
const finalize = () => {
|
|
278
|
+
isFinalized = true;
|
|
279
|
+
stmt.finalize();
|
|
280
|
+
};
|
|
281
|
+
const preparedStatement = {
|
|
282
|
+
execute,
|
|
283
|
+
finalize,
|
|
284
|
+
get isFinalized() {
|
|
285
|
+
return isFinalized;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
this.preparedStatements.push(preparedStatement);
|
|
289
|
+
return preparedStatement;
|
|
290
|
+
}
|
|
291
|
+
prepareKysely(opts) {
|
|
292
|
+
return (factory) => {
|
|
293
|
+
const query = factory(dummyKysely, (key) => key).compile();
|
|
294
|
+
const statement = this.prepare(query.sql, opts);
|
|
295
|
+
return {
|
|
296
|
+
execute: (parameters) => {
|
|
297
|
+
const params = query.parameters.map((param) => parameters[param]);
|
|
298
|
+
const result = statement.execute(params);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
executeKysely(factory, meta) {
|
|
305
|
+
const query = factory(dummyKysely).compile();
|
|
306
|
+
return this.execute(query, meta);
|
|
307
|
+
}
|
|
308
|
+
executePrepared(key, params, factory, meta) {
|
|
309
|
+
let statement = this.preparedStatementsMap.get(key);
|
|
310
|
+
if (!statement) {
|
|
311
|
+
statement = this.prepareKysely(meta)(factory);
|
|
312
|
+
this.preparedStatementsMap.set(key, statement);
|
|
313
|
+
}
|
|
314
|
+
return statement.execute(params);
|
|
315
|
+
}
|
|
316
|
+
executePreparedRaw({
|
|
317
|
+
key,
|
|
318
|
+
sql: sql2,
|
|
319
|
+
params,
|
|
320
|
+
meta
|
|
321
|
+
}) {
|
|
322
|
+
let statement = this.preparedRawStatementsMap.get(key);
|
|
323
|
+
if (!statement) {
|
|
324
|
+
statement = this.prepare(sql2, meta);
|
|
325
|
+
this.preparedRawStatementsMap.set(key, statement);
|
|
326
|
+
}
|
|
327
|
+
return statement.execute(params ?? []);
|
|
328
|
+
}
|
|
329
|
+
sql(templateOrString, ...parameters) {
|
|
330
|
+
if (typeof templateOrString === "string") {
|
|
331
|
+
return this.execute({
|
|
332
|
+
sql: templateOrString,
|
|
333
|
+
parameters
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return this.execute({
|
|
337
|
+
sql: templateOrString.join("?"),
|
|
338
|
+
parameters
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
createScalarFunction({
|
|
342
|
+
name,
|
|
343
|
+
callback,
|
|
344
|
+
deterministic,
|
|
345
|
+
directOnly,
|
|
346
|
+
innocuous
|
|
347
|
+
}) {
|
|
348
|
+
return this.ensureDb.createFunction({
|
|
349
|
+
name,
|
|
350
|
+
xFunc: (_, ...args) => {
|
|
351
|
+
const result = callback(...args);
|
|
352
|
+
return result;
|
|
353
|
+
},
|
|
354
|
+
arity: callback.length,
|
|
355
|
+
deterministic,
|
|
356
|
+
directOnly,
|
|
357
|
+
innocuous
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
useSnapshot(snapshot) {
|
|
361
|
+
const perf = this.logger ? startPerformanceLogger(this.logger) : void 0;
|
|
362
|
+
const dataPointer = this.sqlite3.wasm.allocFromTypedArray(snapshot);
|
|
363
|
+
this.dataPointers.push(dataPointer);
|
|
364
|
+
const resultCode = this.sqlite3.capi.sqlite3_deserialize(
|
|
365
|
+
this.ensureDb,
|
|
366
|
+
"main",
|
|
367
|
+
dataPointer,
|
|
368
|
+
snapshot.byteLength,
|
|
369
|
+
snapshot.byteLength,
|
|
370
|
+
this.sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE | this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE
|
|
371
|
+
);
|
|
372
|
+
this.ensureDb.checkRc(resultCode);
|
|
373
|
+
this.invalidateDbSchema();
|
|
374
|
+
perf?.logEnd("useSnapshot", "success", "system");
|
|
375
|
+
}
|
|
376
|
+
createSnapshot() {
|
|
377
|
+
return this.sqlite3.capi.sqlite3_js_db_export(this.ensureDb);
|
|
378
|
+
}
|
|
379
|
+
invalidateDbSchema() {
|
|
380
|
+
this.loadedDbSchema = null;
|
|
381
|
+
}
|
|
382
|
+
cleanup() {
|
|
383
|
+
this.preparedStatements.forEach((stmt) => {
|
|
384
|
+
stmt.finalize();
|
|
385
|
+
});
|
|
386
|
+
this.preparedStatements.splice(0);
|
|
387
|
+
this.preparedStatementsMap.clear();
|
|
388
|
+
this.preparedRawStatementsMap.clear();
|
|
389
|
+
}
|
|
390
|
+
close() {
|
|
391
|
+
this.cleanup();
|
|
392
|
+
this.db?.close();
|
|
393
|
+
this.db = null;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// src/migrations/migrator.ts
|
|
398
|
+
var protectedColumns = ["id", "tombstone"];
|
|
399
|
+
function assertColumnNotProtected(column, operation) {
|
|
400
|
+
if (protectedColumns.includes(column)) {
|
|
401
|
+
throw new Error(`Cannot ${operation} protected column "${column}"`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
var migrationSteps = {
|
|
405
|
+
createTable: (table, build) => ({
|
|
406
|
+
sql: (db) => build(db.schema.createTable(table))
|
|
407
|
+
}),
|
|
408
|
+
dropTable: (table) => ({
|
|
409
|
+
sql: (db) => db.schema.dropTable(table),
|
|
410
|
+
eventTransformer: {
|
|
411
|
+
[table]: () => null
|
|
412
|
+
},
|
|
413
|
+
tableDrops: [table]
|
|
414
|
+
}),
|
|
415
|
+
createIndex: (indexName, build) => ({
|
|
416
|
+
sql: (db) => build(db.schema.createIndex(indexName))
|
|
417
|
+
}),
|
|
418
|
+
dropIndex: (indexName) => ({
|
|
419
|
+
sql: (db) => db.schema.dropIndex(indexName)
|
|
420
|
+
}),
|
|
421
|
+
renameTable: ({ oldTable, newTable }) => ({
|
|
422
|
+
sql: (db) => db.schema.alterTable(oldTable).renameTo(newTable),
|
|
423
|
+
eventTransformer: {
|
|
424
|
+
[oldTable]: (event) => {
|
|
425
|
+
event.dataset = newTable;
|
|
426
|
+
return event;
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
tableRenames: [{ oldTable, newTable }]
|
|
430
|
+
}),
|
|
431
|
+
renameColumn: ({
|
|
432
|
+
table,
|
|
433
|
+
oldColumn,
|
|
434
|
+
newColumn
|
|
435
|
+
}) => {
|
|
436
|
+
assertColumnNotProtected(oldColumn, "rename");
|
|
437
|
+
assertColumnNotProtected(newColumn, "rename to");
|
|
438
|
+
return {
|
|
439
|
+
sql: (db) => db.schema.alterTable(table).renameColumn(oldColumn, newColumn),
|
|
440
|
+
eventTransformer: {
|
|
441
|
+
[table]: (event) => {
|
|
442
|
+
if (event.type !== "item-updated" && event.type !== "item-created" || !(oldColumn in event.payload)) {
|
|
443
|
+
return event;
|
|
444
|
+
}
|
|
445
|
+
const oldVal = event.payload[oldColumn];
|
|
446
|
+
delete event.payload[oldColumn];
|
|
447
|
+
event.payload[newColumn] = oldVal;
|
|
448
|
+
return event;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
addColumn: ({
|
|
454
|
+
table,
|
|
455
|
+
column,
|
|
456
|
+
type,
|
|
457
|
+
defaultValue,
|
|
458
|
+
build = (e) => e
|
|
459
|
+
}) => ({
|
|
460
|
+
sql: (db) => db.schema.alterTable(table).addColumn(column, type, (x) => build(x).defaultTo(defaultValue)),
|
|
461
|
+
eventTransformer: {
|
|
462
|
+
[table]: (event) => {
|
|
463
|
+
if (event.type !== "item-created") {
|
|
464
|
+
return event;
|
|
465
|
+
}
|
|
466
|
+
event.payload[column] = defaultValue;
|
|
467
|
+
return event;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}),
|
|
471
|
+
dropColumn: ({ table, column }) => {
|
|
472
|
+
assertColumnNotProtected(column, "drop");
|
|
473
|
+
return {
|
|
474
|
+
sql: (db) => db.schema.alterTable(table).dropColumn(column),
|
|
475
|
+
eventTransformer: {
|
|
476
|
+
[table]: (event) => {
|
|
477
|
+
if (!(column in event.payload)) {
|
|
478
|
+
return event;
|
|
479
|
+
}
|
|
480
|
+
delete event.payload[column];
|
|
481
|
+
if (event.type === "item-updated" && Object.keys(event.payload).length === 0) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return event;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
function buildMigrationSql(steps) {
|
|
491
|
+
return steps.flatMap((step) => Array.isArray(step.sql) ? step.sql : [step.sql]).map((sql2) => {
|
|
492
|
+
if (typeof sql2 === "string") {
|
|
493
|
+
return { sql: sql2, parameters: [] };
|
|
494
|
+
}
|
|
495
|
+
if (typeof sql2 === "function") {
|
|
496
|
+
const query = sql2(dummyKysely).compile();
|
|
497
|
+
return { sql: query.sql, parameters: query.parameters };
|
|
498
|
+
}
|
|
499
|
+
if ("compile" in sql2) {
|
|
500
|
+
const query = sql2.compile();
|
|
501
|
+
return { sql: query.sql, parameters: query.parameters };
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
sql: sql2.sql,
|
|
505
|
+
parameters: sql2.parameters ?? []
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function buildMigrationEventTransformer(steps) {
|
|
510
|
+
const transformers = [];
|
|
511
|
+
for (const step of steps) {
|
|
512
|
+
if (step.eventTransformer) {
|
|
513
|
+
transformers.push(...Object.entries(step.eventTransformer));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (transformers.length === 0) {
|
|
517
|
+
return void 0;
|
|
518
|
+
}
|
|
519
|
+
return (event) => {
|
|
520
|
+
let transformedEvent = event;
|
|
521
|
+
for (const [table, transformer] of transformers) {
|
|
522
|
+
if (transformedEvent === null) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
if (transformedEvent.dataset !== table) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
transformedEvent = transformer(transformedEvent);
|
|
529
|
+
if (transformedEvent === null) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return transformedEvent;
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function createMigrations(buildMigrations) {
|
|
537
|
+
const migrations = Object.fromEntries(
|
|
538
|
+
Object.entries(buildMigrations(migrationSteps)).map(([version, steps]) => {
|
|
539
|
+
const versionNumber = Number(version);
|
|
540
|
+
if (Number.isNaN(versionNumber)) {
|
|
541
|
+
throw new Error(`Invalid migration version: ${version}`);
|
|
542
|
+
}
|
|
543
|
+
if (versionNumber < 0) {
|
|
544
|
+
throw new Error(`Migration version cannot be negative: ${version}`);
|
|
545
|
+
}
|
|
546
|
+
const tableRenames = steps.flatMap((s) => s.tableRenames ?? []);
|
|
547
|
+
const tableDrops = steps.flatMap((s) => s.tableDrops ?? []);
|
|
548
|
+
return [
|
|
549
|
+
version,
|
|
550
|
+
{
|
|
551
|
+
sql: buildMigrationSql(steps),
|
|
552
|
+
eventTransformer: buildMigrationEventTransformer(steps),
|
|
553
|
+
...tableRenames.length > 0 && { tableRenames },
|
|
554
|
+
...tableDrops.length > 0 && { tableDrops }
|
|
555
|
+
}
|
|
556
|
+
];
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
return migrations;
|
|
560
|
+
}
|
|
561
|
+
function createMigrator({
|
|
562
|
+
migrations,
|
|
563
|
+
schemaVersion,
|
|
564
|
+
updateLogTableName
|
|
565
|
+
}) {
|
|
566
|
+
const latestSchemaVersion = Math.max(...Object.keys(migrations).map(Number));
|
|
567
|
+
const sortedMigrations = Object.entries(migrations).map(([v, m]) => [Number(v), m]).sort((a, b) => a[0] - b[0]);
|
|
568
|
+
const applyMigration = (db, version, migration) => {
|
|
569
|
+
if (version <= schemaVersion.current) {
|
|
570
|
+
throw new Error(`Cannot apply migration ${version} to schema version ${schemaVersion.current}`);
|
|
571
|
+
}
|
|
572
|
+
db.startTransaction((tx) => {
|
|
573
|
+
for (const statement of migration.sql) {
|
|
574
|
+
tx.execute(statement.sql, statement.parameters);
|
|
575
|
+
}
|
|
576
|
+
if (updateLogTableName) {
|
|
577
|
+
if (migration.tableRenames) {
|
|
578
|
+
for (const { oldTable, newTable } of migration.tableRenames) {
|
|
579
|
+
tx.execute(`UPDATE ${updateLogTableName} SET "dataset" = ? WHERE "dataset" = ?`, [newTable, oldTable]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (migration.tableDrops) {
|
|
583
|
+
for (const table of migration.tableDrops) {
|
|
584
|
+
tx.execute(`DELETE FROM ${updateLogTableName} WHERE "dataset" = ?`, [table]);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
schemaVersion.current = version;
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
const migrateEvent = (event, targetVersion) => {
|
|
592
|
+
targetVersion ??= latestSchemaVersion;
|
|
593
|
+
if (targetVersion > schemaVersion.current) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Target schema version ${targetVersion} is greater than current schema version ${schemaVersion.current}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (event.schema_version >= targetVersion) {
|
|
599
|
+
return event;
|
|
600
|
+
}
|
|
601
|
+
const fromVersion = event.schema_version;
|
|
602
|
+
let crdtEvent = {
|
|
603
|
+
dataset: event.dataset,
|
|
604
|
+
item_id: event.item_id,
|
|
605
|
+
type: event.type,
|
|
606
|
+
payload: JSON.parse(event.payload)
|
|
607
|
+
};
|
|
608
|
+
for (let i = 0; i < sortedMigrations.length; i++) {
|
|
609
|
+
const [version, migration] = sortedMigrations[i];
|
|
610
|
+
if (version <= fromVersion) continue;
|
|
611
|
+
if (version > targetVersion) break;
|
|
612
|
+
const transformer = migration.eventTransformer;
|
|
613
|
+
if (transformer) {
|
|
614
|
+
crdtEvent = transformer(crdtEvent);
|
|
615
|
+
if (crdtEvent === null) return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (crdtEvent === null) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
event.schema_version = targetVersion;
|
|
622
|
+
event.dataset = crdtEvent.dataset;
|
|
623
|
+
event.item_id = crdtEvent.item_id;
|
|
624
|
+
event.type = crdtEvent.type;
|
|
625
|
+
event.payload = JSON.stringify(crdtEvent.payload);
|
|
626
|
+
return event;
|
|
627
|
+
};
|
|
628
|
+
const migrateEvents = (events, targetVersion) => {
|
|
629
|
+
return events.map((event) => migrateEvent(event, targetVersion ?? latestSchemaVersion)).filter((event) => event !== null);
|
|
630
|
+
};
|
|
631
|
+
return {
|
|
632
|
+
latestSchemaVersion,
|
|
633
|
+
get currentSchemaVersion() {
|
|
634
|
+
return schemaVersion.current;
|
|
635
|
+
},
|
|
636
|
+
migrateDbToLatest: (db) => {
|
|
637
|
+
const currentSchemaVersion = schemaVersion.current;
|
|
638
|
+
if (currentSchemaVersion >= latestSchemaVersion) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
for (let i = 0; i < sortedMigrations.length; i++) {
|
|
642
|
+
const [version, migration] = sortedMigrations[i];
|
|
643
|
+
if (version <= currentSchemaVersion) continue;
|
|
644
|
+
applyMigration(db, version, migration);
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
migrateEvent,
|
|
648
|
+
migrateEvents
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/sqlite-crdt/stored-value.ts
|
|
653
|
+
function createStoredValue({
|
|
654
|
+
initialValue,
|
|
655
|
+
saveToStorage
|
|
656
|
+
}) {
|
|
657
|
+
let currentValue = initialValue;
|
|
658
|
+
return {
|
|
659
|
+
get current() {
|
|
660
|
+
return currentValue;
|
|
661
|
+
},
|
|
662
|
+
set current(newValue) {
|
|
663
|
+
saveToStorage?.(newValue);
|
|
664
|
+
currentValue = newValue;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/sqlite-kv-store.ts
|
|
670
|
+
function createKvStoreTableQuery(schema, tableName) {
|
|
671
|
+
return schema.createTable(tableName).ifNotExists().addColumn("key", "text", (col) => col.notNull().primaryKey()).addColumn("value", "text", (col) => col.notNull());
|
|
672
|
+
}
|
|
673
|
+
function createSQLiteKvStore({
|
|
674
|
+
db,
|
|
675
|
+
metaTableName
|
|
676
|
+
}) {
|
|
677
|
+
const metaDb = db;
|
|
678
|
+
const get = (key) => {
|
|
679
|
+
const [result] = metaDb.executePrepared(
|
|
680
|
+
"get-meta-value",
|
|
681
|
+
{ key },
|
|
682
|
+
(db2, params) => db2.selectFrom(metaTableName).where("key", "=", params("key")).select("value").limit((eb) => eb.lit(1)),
|
|
683
|
+
{ loggerLevel: "system" }
|
|
684
|
+
);
|
|
685
|
+
return result?.value ?? null;
|
|
686
|
+
};
|
|
687
|
+
const set = (key, value) => {
|
|
688
|
+
metaDb.executePrepared(
|
|
689
|
+
"set-meta-value",
|
|
690
|
+
{ key, value },
|
|
691
|
+
(db2, params) => db2.insertInto(metaTableName).values({ key: params("key"), value: params("value") }).onConflict((oc) => oc.doUpdateSet({ value: params("value") })),
|
|
692
|
+
{ loggerLevel: "system" }
|
|
693
|
+
);
|
|
694
|
+
};
|
|
695
|
+
const remove = (key) => {
|
|
696
|
+
metaDb.executePrepared(
|
|
697
|
+
"remove-meta-value",
|
|
698
|
+
{ key },
|
|
699
|
+
(db2, params) => db2.deleteFrom(metaTableName).where("key", "=", params("key")),
|
|
700
|
+
{ loggerLevel: "system" }
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
const getNumberOrDefault = (key, defaultValue) => {
|
|
704
|
+
const value = get(key);
|
|
705
|
+
if (!value) return defaultValue;
|
|
706
|
+
const parsedValue = Number.parseInt(value, 10);
|
|
707
|
+
return Number.isNaN(parsedValue) ? defaultValue : parsedValue;
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
get,
|
|
711
|
+
set,
|
|
712
|
+
remove,
|
|
713
|
+
createStringStoredValue: (key, defaultValue) => createStoredValue({
|
|
714
|
+
initialValue: get(key) ?? defaultValue,
|
|
715
|
+
saveToStorage: (val) => set(key, val)
|
|
716
|
+
}),
|
|
717
|
+
createNumberStoredValue: (key, defaultValue) => createStoredValue({
|
|
718
|
+
initialValue: getNumberOrDefault(key, defaultValue),
|
|
719
|
+
saveToStorage: (val) => set(key, val.toString())
|
|
720
|
+
})
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/migrations/system-schema.ts
|
|
725
|
+
var baseSystemMigrations = [
|
|
726
|
+
{
|
|
727
|
+
version: 0,
|
|
728
|
+
up: (ctx) => {
|
|
729
|
+
ctx.execute(`CREATE TABLE IF NOT EXISTS ${ctx.eventsTableName} (
|
|
730
|
+
"sync_id" integer NOT NULL PRIMARY KEY,
|
|
731
|
+
"schema_version" integer NOT NULL,
|
|
732
|
+
"status" text NOT NULL,
|
|
733
|
+
"type" text NOT NULL,
|
|
734
|
+
"timestamp" text NOT NULL,
|
|
735
|
+
"origin" text NOT NULL,
|
|
736
|
+
"dataset" text NOT NULL,
|
|
737
|
+
"item_id" text NOT NULL,
|
|
738
|
+
"payload" text NOT NULL
|
|
739
|
+
)`);
|
|
740
|
+
ctx.execute(`CREATE TABLE IF NOT EXISTS ${ctx.updateLogTableName} (
|
|
741
|
+
"dataset" text NOT NULL,
|
|
742
|
+
"item_id" text NOT NULL,
|
|
743
|
+
"payload" text NOT NULL,
|
|
744
|
+
PRIMARY KEY ("item_id", "dataset")
|
|
745
|
+
)`);
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
version: 1,
|
|
750
|
+
up: (ctx) => {
|
|
751
|
+
ctx.execute(`ALTER TABLE ${ctx.eventsTableName} ADD COLUMN "source_node_id" TEXT NOT NULL DEFAULT ''`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
];
|
|
755
|
+
function runSystemMigrations(opts) {
|
|
756
|
+
const ctx = {
|
|
757
|
+
eventsTableName: opts.eventsTableName,
|
|
758
|
+
updateLogTableName: opts.updateLogTableName,
|
|
759
|
+
execute: opts.execute
|
|
760
|
+
};
|
|
761
|
+
for (const migration of opts.migrations) {
|
|
762
|
+
if (migration.version > opts.version.current) {
|
|
763
|
+
opts.transaction(() => {
|
|
764
|
+
migration.up(ctx);
|
|
765
|
+
opts.version.current = migration.version;
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function applyWorkerDbSchema(db) {
|
|
771
|
+
db.executeKysely((kysely) => createKvStoreTableQuery(kysely.schema, "worker.kv"), { loggerLevel: "system" });
|
|
772
|
+
const kvStore = createSQLiteKvStore({ db, metaTableName: "worker.kv" });
|
|
773
|
+
runSystemMigrations({
|
|
774
|
+
migrations: baseSystemMigrations,
|
|
775
|
+
version: kvStore.createNumberStoredValue("internal-schema-version", -1),
|
|
776
|
+
eventsTableName: '"worker"."crdt_events"',
|
|
777
|
+
updateLogTableName: '"crdt_update_log"',
|
|
778
|
+
execute: (sql2) => db.execute(sql2, { loggerLevel: "system" }),
|
|
779
|
+
transaction: (callback) => db.executeTransaction(callback)
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function applyMemoryDbSchema(db) {
|
|
783
|
+
db.execute(
|
|
784
|
+
`CREATE TABLE "persisted_crdt_events" (
|
|
785
|
+
"sync_id" integer NOT NULL PRIMARY KEY,
|
|
786
|
+
"schema_version" integer NOT NULL,
|
|
787
|
+
"status" text NOT NULL,
|
|
788
|
+
"type" text NOT NULL,
|
|
789
|
+
"timestamp" text NOT NULL,
|
|
790
|
+
"origin" text NOT NULL,
|
|
791
|
+
"source_node_id" text NOT NULL DEFAULT '',
|
|
792
|
+
"dataset" text NOT NULL,
|
|
793
|
+
"item_id" text NOT NULL,
|
|
794
|
+
"payload" text NOT NULL
|
|
795
|
+
)`,
|
|
796
|
+
{ loggerLevel: "system" }
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/sqlite-crdt/apply-crdt-event.ts
|
|
801
|
+
var createSQLiteCrdtApplyFunction = ({
|
|
802
|
+
db,
|
|
803
|
+
updateLogTableName
|
|
804
|
+
}) => {
|
|
805
|
+
const applyCrdtEvent = createCrdtApplyFunction({
|
|
806
|
+
getCrdtUpdateLog(opts) {
|
|
807
|
+
const [metaRow] = db.executePrepared(
|
|
808
|
+
"get-item-crdt-meta",
|
|
809
|
+
{
|
|
810
|
+
item_id: opts.itemId,
|
|
811
|
+
dataset: opts.dataset
|
|
812
|
+
},
|
|
813
|
+
(db2, params) => {
|
|
814
|
+
return db2.selectFrom(updateLogTableName).select("payload").where("item_id", "=", params("item_id")).where("dataset", "=", params("dataset"));
|
|
815
|
+
},
|
|
816
|
+
{ loggerLevel: "system" }
|
|
817
|
+
);
|
|
818
|
+
const meta = metaRow ? JSON.parse(metaRow.payload) : null;
|
|
819
|
+
return meta;
|
|
820
|
+
},
|
|
821
|
+
insertCrdtUpdateLog(opts) {
|
|
822
|
+
db.executePrepared(
|
|
823
|
+
"insert-crdt-update-log",
|
|
824
|
+
{
|
|
825
|
+
item_id: opts.itemId,
|
|
826
|
+
dataset: opts.dataset,
|
|
827
|
+
payload: opts.payload
|
|
828
|
+
},
|
|
829
|
+
(db2, params) => db2.insertInto(updateLogTableName).values({
|
|
830
|
+
item_id: params("item_id"),
|
|
831
|
+
dataset: params("dataset"),
|
|
832
|
+
payload: params("payload")
|
|
833
|
+
}),
|
|
834
|
+
{ loggerLevel: "system" }
|
|
835
|
+
);
|
|
836
|
+
},
|
|
837
|
+
updateCrdtUpdateLog(opts) {
|
|
838
|
+
db.executePrepared(
|
|
839
|
+
"update-crdt-update-log",
|
|
840
|
+
{
|
|
841
|
+
item_id: opts.itemId,
|
|
842
|
+
dataset: opts.dataset,
|
|
843
|
+
payload: opts.payload
|
|
844
|
+
},
|
|
845
|
+
(db2, params) => db2.updateTable(updateLogTableName).set({
|
|
846
|
+
payload: params("payload")
|
|
847
|
+
}).where("item_id", "=", params("item_id")).where("dataset", "=", params("dataset")),
|
|
848
|
+
{ loggerLevel: "system" }
|
|
849
|
+
);
|
|
850
|
+
},
|
|
851
|
+
insertItem(opts) {
|
|
852
|
+
const insertPayload = {};
|
|
853
|
+
for (const key of Object.keys(opts.payload)) {
|
|
854
|
+
insertPayload[key] = key;
|
|
855
|
+
}
|
|
856
|
+
db.executePrepared(
|
|
857
|
+
`crdt-insert-item-${opts.dataset}`,
|
|
858
|
+
opts.payload,
|
|
859
|
+
(db2) => db2.insertInto(opts.dataset).values(insertPayload),
|
|
860
|
+
{ loggerLevel: "system" }
|
|
861
|
+
);
|
|
862
|
+
},
|
|
863
|
+
updateItem(opts) {
|
|
864
|
+
const keys = Array.from(Object.keys(opts.payload));
|
|
865
|
+
db.execute(
|
|
866
|
+
{
|
|
867
|
+
sql: `update ${quoteId(opts.dataset)} set ${keys.map((key) => `${quoteId(key)} = ?`).join(",")} where id = ?`,
|
|
868
|
+
parameters: [...keys.map((key) => opts.payload[key]), opts.itemId]
|
|
869
|
+
},
|
|
870
|
+
{ loggerLevel: "system" }
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
return applyCrdtEvent;
|
|
875
|
+
};
|
|
876
|
+
function createCrdtApplyFunction({
|
|
877
|
+
getCrdtUpdateLog,
|
|
878
|
+
insertItem,
|
|
879
|
+
insertCrdtUpdateLog,
|
|
880
|
+
updateItem,
|
|
881
|
+
updateCrdtUpdateLog
|
|
882
|
+
}) {
|
|
883
|
+
const applyItemCreated = ({ event, meta }) => {
|
|
884
|
+
if (meta) {
|
|
885
|
+
applyItemUpdated({ event, meta });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const eventPayload = JSON.parse(event.payload);
|
|
889
|
+
eventPayload.tombstone = false;
|
|
890
|
+
insertItem({ dataset: event.dataset, payload: eventPayload });
|
|
891
|
+
const newUpdateLog = {};
|
|
892
|
+
for (const key of Object.keys(eventPayload)) {
|
|
893
|
+
newUpdateLog[key] = event.timestamp;
|
|
894
|
+
}
|
|
895
|
+
insertCrdtUpdateLog({
|
|
896
|
+
dataset: event.dataset,
|
|
897
|
+
itemId: event.item_id,
|
|
898
|
+
payload: JSON.stringify(newUpdateLog)
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
const applyItemUpdated = ({ event, meta }) => {
|
|
902
|
+
if (!meta) {
|
|
903
|
+
throw new Error(`Item ${event.item_id} in dataset ${event.dataset} not found`);
|
|
904
|
+
}
|
|
905
|
+
const eventPayload = JSON.parse(event.payload);
|
|
906
|
+
const updatePayload = {};
|
|
907
|
+
let hasUpdates = false;
|
|
908
|
+
for (const [key, value] of Object.entries(eventPayload)) {
|
|
909
|
+
const lastUpdateTimestamp = meta[key];
|
|
910
|
+
const currentUpdateTimestamp = event.timestamp;
|
|
911
|
+
if (!lastUpdateTimestamp || !currentUpdateTimestamp || currentUpdateTimestamp > lastUpdateTimestamp) {
|
|
912
|
+
updatePayload[key] = value;
|
|
913
|
+
meta[key] = currentUpdateTimestamp;
|
|
914
|
+
hasUpdates = true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!hasUpdates) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
updateItem({
|
|
921
|
+
dataset: event.dataset,
|
|
922
|
+
itemId: event.item_id,
|
|
923
|
+
payload: updatePayload
|
|
924
|
+
});
|
|
925
|
+
updateCrdtUpdateLog({
|
|
926
|
+
dataset: event.dataset,
|
|
927
|
+
itemId: event.item_id,
|
|
928
|
+
payload: JSON.stringify(meta)
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
return (event) => {
|
|
932
|
+
const meta = getCrdtUpdateLog({
|
|
933
|
+
itemId: event.item_id,
|
|
934
|
+
dataset: event.dataset
|
|
935
|
+
});
|
|
936
|
+
switch (event.type) {
|
|
937
|
+
case "item-created": {
|
|
938
|
+
applyItemCreated({
|
|
939
|
+
event,
|
|
940
|
+
meta
|
|
941
|
+
});
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
case "item-updated": {
|
|
945
|
+
if (!meta) {
|
|
946
|
+
throw new Error(`Item ${event.item_id} in dataset ${event.dataset} not found`);
|
|
947
|
+
}
|
|
948
|
+
applyItemUpdated({
|
|
949
|
+
event,
|
|
950
|
+
meta
|
|
951
|
+
});
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
default:
|
|
955
|
+
event.type;
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/sqlite-crdt/crdt-storage.ts
|
|
961
|
+
function createCrdtStorage(storage) {
|
|
962
|
+
const transaction = storage.transaction ?? ((callback) => callback());
|
|
963
|
+
const eventTarget = createTypedEventTarget();
|
|
964
|
+
const enqueueEvents = (origin, sourceNodeId, events) => {
|
|
965
|
+
if (events.length === 0) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
transaction(() => {
|
|
969
|
+
for (const event of events) {
|
|
970
|
+
storage.persistEvent({
|
|
971
|
+
schema_version: event.schema_version ?? storage.migrator.currentSchemaVersion,
|
|
972
|
+
timestamp: event.timestamp ?? serializeHLC(storage.hlc.getNextHLC()),
|
|
973
|
+
type: event.type,
|
|
974
|
+
dataset: event.dataset,
|
|
975
|
+
item_id: event.item_id,
|
|
976
|
+
origin,
|
|
977
|
+
source_node_id: sourceNodeId,
|
|
978
|
+
payload: event.payload,
|
|
979
|
+
sync_id: ++storage.syncId.current,
|
|
980
|
+
status: "pending"
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
processEnqueuedEvents();
|
|
985
|
+
};
|
|
986
|
+
const enqueueLocalEvents = (events, sourceNodeId) => {
|
|
987
|
+
enqueueEvents("local", sourceNodeId, events);
|
|
988
|
+
};
|
|
989
|
+
const enqueueOwnEvents = (events) => {
|
|
990
|
+
enqueueEvents("own", storage.nodeId, events);
|
|
991
|
+
};
|
|
992
|
+
const enqueueRemoteEvents = (events) => {
|
|
993
|
+
enqueueEvents("remote", "", events);
|
|
994
|
+
};
|
|
995
|
+
const applyOwnEvent = (event, { wrapInTransaction } = {}) => {
|
|
996
|
+
const persistedEvent = {
|
|
997
|
+
schema_version: storage.migrator.currentSchemaVersion,
|
|
998
|
+
timestamp: serializeHLC(storage.hlc.getNextHLC()),
|
|
999
|
+
type: event.type,
|
|
1000
|
+
dataset: event.dataset,
|
|
1001
|
+
item_id: event.item_id,
|
|
1002
|
+
origin: "own",
|
|
1003
|
+
source_node_id: storage.nodeId,
|
|
1004
|
+
payload: event.payload,
|
|
1005
|
+
sync_id: ++storage.syncId.current,
|
|
1006
|
+
status: "pending"
|
|
1007
|
+
};
|
|
1008
|
+
if (wrapInTransaction) {
|
|
1009
|
+
transaction(() => {
|
|
1010
|
+
storage.persistEvent(persistedEvent);
|
|
1011
|
+
processPersistedEvent(persistedEvent);
|
|
1012
|
+
});
|
|
1013
|
+
} else {
|
|
1014
|
+
storage.persistEvent(persistedEvent);
|
|
1015
|
+
processPersistedEvent(persistedEvent);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
const dispatchEventsApplied = () => {
|
|
1019
|
+
eventTarget.dispatchEvent("events-applied", {
|
|
1020
|
+
syncId: storage.syncId.current
|
|
1021
|
+
});
|
|
1022
|
+
};
|
|
1023
|
+
const getEventsBatch = (options) => {
|
|
1024
|
+
const limit = options.limit ?? 50;
|
|
1025
|
+
const events = storage.getEventsBatch({
|
|
1026
|
+
...options,
|
|
1027
|
+
limit: limit + 1
|
|
1028
|
+
});
|
|
1029
|
+
const hasMore = events.length > limit;
|
|
1030
|
+
if (hasMore) {
|
|
1031
|
+
events.pop();
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
events,
|
|
1035
|
+
hasMore,
|
|
1036
|
+
nextSyncId: events[events.length - 1]?.sync_id ?? options.afterSyncId ?? 0
|
|
1037
|
+
};
|
|
1038
|
+
};
|
|
1039
|
+
const processPersistedEvent = (event) => {
|
|
1040
|
+
if (event.status !== "pending") {
|
|
1041
|
+
throw new Error(`Event ${event.sync_id} is not pending`);
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
if (event.origin === "local" || event.origin === "remote") {
|
|
1045
|
+
storage.hlc.mergeHLC(deserializeHLC(event.timestamp));
|
|
1046
|
+
}
|
|
1047
|
+
const migratedEvent = storage.migrator.migrateEvent(event, storage.migrator.latestSchemaVersion);
|
|
1048
|
+
if (migratedEvent === null) {
|
|
1049
|
+
event.status = "skipped";
|
|
1050
|
+
event.schema_version = storage.migrator.latestSchemaVersion;
|
|
1051
|
+
return event;
|
|
1052
|
+
}
|
|
1053
|
+
event.schema_version = migratedEvent.schema_version;
|
|
1054
|
+
event.type = migratedEvent.type;
|
|
1055
|
+
event.dataset = migratedEvent.dataset;
|
|
1056
|
+
event.item_id = migratedEvent.item_id;
|
|
1057
|
+
event.payload = migratedEvent.payload;
|
|
1058
|
+
storage.handleCrdtEventApply(event);
|
|
1059
|
+
event.status = "applied";
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
console.error("Error applying enqueued CRDT event", error);
|
|
1062
|
+
event.status = "failed";
|
|
1063
|
+
} finally {
|
|
1064
|
+
storage.updateEvent(event.sync_id, {
|
|
1065
|
+
status: event.status,
|
|
1066
|
+
schema_version: event.schema_version,
|
|
1067
|
+
type: event.type,
|
|
1068
|
+
dataset: event.dataset,
|
|
1069
|
+
item_id: event.item_id,
|
|
1070
|
+
payload: event.payload
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const processEnqueuedEvents = ensureSingletonExecution(async () => {
|
|
1075
|
+
let hasMore = true;
|
|
1076
|
+
while (hasMore) {
|
|
1077
|
+
await Promise.resolve();
|
|
1078
|
+
const batch = getEventsBatch({ status: "pending", limit: 100 });
|
|
1079
|
+
const events = batch.events;
|
|
1080
|
+
hasMore = batch.hasMore;
|
|
1081
|
+
if (events.length === 0) {
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
for (const event of events) {
|
|
1085
|
+
transaction(() => {
|
|
1086
|
+
processPersistedEvent(event);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
dispatchEventsApplied();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
return {
|
|
1093
|
+
getEventsBatch,
|
|
1094
|
+
enqueueLocalEvents,
|
|
1095
|
+
enqueueOwnEvents,
|
|
1096
|
+
enqueueRemoteEvents,
|
|
1097
|
+
applyOwnEvent,
|
|
1098
|
+
dispatchEventsApplied,
|
|
1099
|
+
addEventListener: eventTarget.addEventListener,
|
|
1100
|
+
removeEventListener: eventTarget.removeEventListener
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/sqlite-crdt/crdt-sync-producer.ts
|
|
1105
|
+
var createCrdtSyncProducer = ({ storage, broadcastEvents }) => {
|
|
1106
|
+
storage.addEventListener("events-applied", (event) => {
|
|
1107
|
+
broadcastEvents({ newSyncId: event.payload.syncId });
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
// src/sqlite-crdt/crdt-sync-remote-source.ts
|
|
1112
|
+
import retryAsPromised from "retry-as-promised";
|
|
1113
|
+
var createCrdtSyncRemoteSource = ({
|
|
1114
|
+
bufferSize,
|
|
1115
|
+
storage,
|
|
1116
|
+
migrator,
|
|
1117
|
+
pullSyncId,
|
|
1118
|
+
pushSyncId,
|
|
1119
|
+
nodeId,
|
|
1120
|
+
remoteFactory
|
|
1121
|
+
}) => {
|
|
1122
|
+
const eventTarget = createTypedEventTarget();
|
|
1123
|
+
let remoteState = { type: "offline", reason: "NOT_INITIALIZED" };
|
|
1124
|
+
const setRemoteState = (state) => {
|
|
1125
|
+
remoteState = state;
|
|
1126
|
+
eventTarget.dispatchEvent("state-changed", state.type);
|
|
1127
|
+
};
|
|
1128
|
+
const initRemote = ensureSingletonExecution(
|
|
1129
|
+
async () => {
|
|
1130
|
+
if (remoteState.type !== "offline") {
|
|
1131
|
+
throw new Error("Remote source is not offline");
|
|
1132
|
+
}
|
|
1133
|
+
if (!remoteFactory) {
|
|
1134
|
+
console.warn("Remote source factory not provided. Going offline.");
|
|
1135
|
+
setRemoteState({ type: "offline", reason: "NOT_INITIALIZED" });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
setRemoteState({ type: "pending" });
|
|
1139
|
+
const factoryResult = await tryCatchAsync(async () => {
|
|
1140
|
+
return await remoteFactory?.({
|
|
1141
|
+
onEventsAvailable: (newSyncId) => {
|
|
1142
|
+
pullEvents({ remoteSyncId: newSyncId, includeSelf: false });
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
if (!factoryResult.success) {
|
|
1147
|
+
setRemoteState({ type: "offline", reason: "INITIALIZATION_FAILED" });
|
|
1148
|
+
console.warn("Failed to create remote source", factoryResult.error);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
setRemoteState({
|
|
1152
|
+
type: "online",
|
|
1153
|
+
source: factoryResult.data
|
|
1154
|
+
});
|
|
1155
|
+
},
|
|
1156
|
+
{ queueReExecution: false }
|
|
1157
|
+
);
|
|
1158
|
+
const syncWithRemote = ensureSingletonExecution(
|
|
1159
|
+
async () => {
|
|
1160
|
+
if (remoteState.type !== "online") {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
await pullEvents();
|
|
1164
|
+
await startPushingEvents();
|
|
1165
|
+
},
|
|
1166
|
+
{ queueReExecution: false }
|
|
1167
|
+
);
|
|
1168
|
+
const goOffline = ensureSingletonExecution(
|
|
1169
|
+
async (reason) => {
|
|
1170
|
+
if (remoteState.type !== "online") {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const source = remoteState.source;
|
|
1174
|
+
setRemoteState({ type: "pending" });
|
|
1175
|
+
const disconnectResult = await tryCatchAsync(async () => {
|
|
1176
|
+
return await source.disconnect?.();
|
|
1177
|
+
});
|
|
1178
|
+
if (!disconnectResult.success) {
|
|
1179
|
+
console.warn("Error while disconnecting from remote source", disconnectResult.error);
|
|
1180
|
+
}
|
|
1181
|
+
setRemoteState({ type: "offline", reason });
|
|
1182
|
+
},
|
|
1183
|
+
{ queueReExecution: false }
|
|
1184
|
+
);
|
|
1185
|
+
const goOnline = async () => {
|
|
1186
|
+
if (remoteState.type !== "online") {
|
|
1187
|
+
await initRemote();
|
|
1188
|
+
}
|
|
1189
|
+
if (remoteState.type === "online") {
|
|
1190
|
+
await syncWithRemote();
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
let requestedPullSyncId = null;
|
|
1194
|
+
let pullPromise = null;
|
|
1195
|
+
const pullEvents = (request) => {
|
|
1196
|
+
if (remoteState.type !== "online") {
|
|
1197
|
+
return Promise.resolve();
|
|
1198
|
+
}
|
|
1199
|
+
const remoteSyncId = request?.remoteSyncId;
|
|
1200
|
+
if (remoteSyncId !== void 0 && remoteSyncId <= pullSyncId.current) {
|
|
1201
|
+
return Promise.resolve();
|
|
1202
|
+
}
|
|
1203
|
+
if (pullPromise) {
|
|
1204
|
+
if (remoteSyncId !== void 0 && (!requestedPullSyncId || requestedPullSyncId < remoteSyncId)) {
|
|
1205
|
+
requestedPullSyncId = remoteSyncId;
|
|
1206
|
+
}
|
|
1207
|
+
return pullPromise;
|
|
1208
|
+
}
|
|
1209
|
+
pullPromise = pullAllEvents({
|
|
1210
|
+
afterSyncId: pullSyncId.current,
|
|
1211
|
+
excludeNodeId: request?.includeSelf ? void 0 : nodeId
|
|
1212
|
+
}).catch((error) => {
|
|
1213
|
+
console.error("Error pulling events. Going offline.", error);
|
|
1214
|
+
goOffline("REMOTE_PULL_ERROR");
|
|
1215
|
+
}).finally(() => {
|
|
1216
|
+
pullPromise = null;
|
|
1217
|
+
const nextTarget = requestedPullSyncId;
|
|
1218
|
+
requestedPullSyncId = null;
|
|
1219
|
+
if (nextTarget && nextTarget > pullSyncId.current) {
|
|
1220
|
+
pullEvents({ remoteSyncId: nextTarget });
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
return pullPromise;
|
|
1224
|
+
};
|
|
1225
|
+
const pullAllEvents = async (opts) => {
|
|
1226
|
+
let hasMore = true;
|
|
1227
|
+
let afterSyncId = opts.afterSyncId;
|
|
1228
|
+
while (hasMore) {
|
|
1229
|
+
if (remoteState.type !== "online") {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const source = remoteState.source;
|
|
1233
|
+
const response = await retryAsPromised(
|
|
1234
|
+
() => source.pullEvents({
|
|
1235
|
+
...opts,
|
|
1236
|
+
afterSyncId
|
|
1237
|
+
}),
|
|
1238
|
+
{
|
|
1239
|
+
max: 3,
|
|
1240
|
+
backoffBase: 100,
|
|
1241
|
+
backoffExponent: 1.5,
|
|
1242
|
+
backoffJitter: 150,
|
|
1243
|
+
timeout: 1e4
|
|
1244
|
+
}
|
|
1245
|
+
);
|
|
1246
|
+
hasMore = response.hasMore;
|
|
1247
|
+
afterSyncId = response.nextSyncId;
|
|
1248
|
+
if (response.events) {
|
|
1249
|
+
storage.enqueueRemoteEvents(
|
|
1250
|
+
response.events.map((x) => {
|
|
1251
|
+
if (x.schema_version > migrator.currentSchemaVersion) {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`Event schema version ${x.schema_version} is greater than current schema version ${migrator.currentSchemaVersion}`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return x;
|
|
1257
|
+
})
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (response.nextSyncId <= pullSyncId.current) {
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
if (response.nextSyncId > pullSyncId.current) {
|
|
1264
|
+
pullSyncId.current = response.nextSyncId;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
const startPushingEvents = ensureSingletonExecution(async () => {
|
|
1269
|
+
while (true) {
|
|
1270
|
+
const eventsBatch = storage.getEventsBatch({
|
|
1271
|
+
status: "applied",
|
|
1272
|
+
afterSyncId: pushSyncId.current,
|
|
1273
|
+
excludeOrigin: "remote",
|
|
1274
|
+
limit: bufferSize
|
|
1275
|
+
});
|
|
1276
|
+
if (eventsBatch.events.length === 0) {
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
if (remoteState.type !== "online") {
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
const source = remoteState.source;
|
|
1283
|
+
const migratedEvents = migrator.migrateEvents(eventsBatch.events);
|
|
1284
|
+
if (migratedEvents.length > 0) {
|
|
1285
|
+
try {
|
|
1286
|
+
await retryAsPromised(
|
|
1287
|
+
() => source.pushEvents({
|
|
1288
|
+
nodeId,
|
|
1289
|
+
events: migratedEvents
|
|
1290
|
+
}),
|
|
1291
|
+
{
|
|
1292
|
+
max: 3,
|
|
1293
|
+
backoffBase: 100,
|
|
1294
|
+
backoffExponent: 1.5,
|
|
1295
|
+
backoffJitter: 150,
|
|
1296
|
+
timeout: 1e4
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
console.error("Error pushing events. Going offline.", error);
|
|
1301
|
+
goOffline("REMOTE_PUSH_ERROR");
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
pushSyncId.current = eventsBatch.nextSyncId;
|
|
1306
|
+
if (!eventsBatch.hasMore) {
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
const onEventsApplied = () => {
|
|
1312
|
+
startPushingEvents();
|
|
1313
|
+
};
|
|
1314
|
+
storage.addEventListener("events-applied", onEventsApplied);
|
|
1315
|
+
const getState = () => remoteState.type;
|
|
1316
|
+
const dispose = async () => {
|
|
1317
|
+
await goOffline("DISCONNECTED");
|
|
1318
|
+
storage.removeEventListener("events-applied", onEventsApplied);
|
|
1319
|
+
};
|
|
1320
|
+
return {
|
|
1321
|
+
goOnline,
|
|
1322
|
+
goOffline,
|
|
1323
|
+
syncWithRemote,
|
|
1324
|
+
getState,
|
|
1325
|
+
dispose,
|
|
1326
|
+
addEventListener: eventTarget.addEventListener,
|
|
1327
|
+
removeEventListener: eventTarget.removeEventListener
|
|
1328
|
+
};
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// src/sqlite-crdt/events-batch-filters.ts
|
|
1332
|
+
function applyKyselyEventsBatchFilters(query, opts) {
|
|
1333
|
+
if (opts.afterSyncId) {
|
|
1334
|
+
query = query.where("sync_id", ">", opts.afterSyncId);
|
|
1335
|
+
}
|
|
1336
|
+
if (opts.status) {
|
|
1337
|
+
query = query.where("status", "=", opts.status);
|
|
1338
|
+
}
|
|
1339
|
+
if (opts.excludeOrigin) {
|
|
1340
|
+
query = query.where("origin", "!=", opts.excludeOrigin);
|
|
1341
|
+
}
|
|
1342
|
+
if (opts.excludeNodeId) {
|
|
1343
|
+
query = query.where("source_node_id", "!=", opts.excludeNodeId);
|
|
1344
|
+
}
|
|
1345
|
+
return query.limit(opts.limit ?? 50).orderBy("sync_id", "asc");
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/worker-db/worker-common.ts
|
|
1349
|
+
var syncDbWorkerLockName = "sync-db-worker-lock";
|
|
1350
|
+
var syncDbClientLockName = "sync-db-client-lock";
|
|
1351
|
+
var broadcastChannelNames = {
|
|
1352
|
+
requests: "sync-db-worker-requests",
|
|
1353
|
+
responses: "sync-db-worker-responses"
|
|
1354
|
+
};
|
|
1355
|
+
var createBroadcastChannels = (prefix) => {
|
|
1356
|
+
return {
|
|
1357
|
+
requests: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.requests}`),
|
|
1358
|
+
responses: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.responses}`)
|
|
1359
|
+
};
|
|
1360
|
+
};
|
|
1361
|
+
function isWorkerInitMessage(message) {
|
|
1362
|
+
return typeof message === "object" && message !== null && "type" in message && message.type === "init";
|
|
1363
|
+
}
|
|
1364
|
+
function isWorkerRequestMessage(message) {
|
|
1365
|
+
return typeof message === "object" && message !== null && "type" in message && message.type === "request";
|
|
1366
|
+
}
|
|
1367
|
+
function isWorkerResponseMessage(message) {
|
|
1368
|
+
return typeof message === "object" && message !== null && "type" in message && "requestId" in message && "data" in message;
|
|
1369
|
+
}
|
|
1370
|
+
function isWorkerErrorResponseMessage(message) {
|
|
1371
|
+
return typeof message === "object" && message !== null && "type" in message && message.type === "error-response";
|
|
1372
|
+
}
|
|
1373
|
+
function isWorkerNotificationMessage(message) {
|
|
1374
|
+
return typeof message === "object" && message !== null && "notificationType" in message && !!message.notificationType;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
export {
|
|
1378
|
+
dummyKysely,
|
|
1379
|
+
HLCCounter,
|
|
1380
|
+
serializeHLC,
|
|
1381
|
+
deserializeHLC,
|
|
1382
|
+
compareHLC,
|
|
1383
|
+
introspectDb,
|
|
1384
|
+
startPerformanceLogger,
|
|
1385
|
+
SQLiteDbWrapper,
|
|
1386
|
+
createMigrations,
|
|
1387
|
+
createMigrator,
|
|
1388
|
+
createStoredValue,
|
|
1389
|
+
createKvStoreTableQuery,
|
|
1390
|
+
createSQLiteKvStore,
|
|
1391
|
+
baseSystemMigrations,
|
|
1392
|
+
runSystemMigrations,
|
|
1393
|
+
applyWorkerDbSchema,
|
|
1394
|
+
applyMemoryDbSchema,
|
|
1395
|
+
createSQLiteCrdtApplyFunction,
|
|
1396
|
+
createCrdtApplyFunction,
|
|
1397
|
+
createCrdtStorage,
|
|
1398
|
+
createCrdtSyncProducer,
|
|
1399
|
+
createCrdtSyncRemoteSource,
|
|
1400
|
+
applyKyselyEventsBatchFilters,
|
|
1401
|
+
syncDbWorkerLockName,
|
|
1402
|
+
syncDbClientLockName,
|
|
1403
|
+
createBroadcastChannels,
|
|
1404
|
+
isWorkerInitMessage,
|
|
1405
|
+
isWorkerRequestMessage,
|
|
1406
|
+
isWorkerResponseMessage,
|
|
1407
|
+
isWorkerErrorResponseMessage,
|
|
1408
|
+
isWorkerNotificationMessage
|
|
1409
|
+
};
|
|
1410
|
+
//# sourceMappingURL=chunk-627DSM2Q.js.map
|