@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/index.js CHANGED
@@ -1,206 +1,98 @@
1
1
  import {
2
+ HLCCounter,
2
3
  SQLiteDbWrapper,
3
- SqliteDriver,
4
+ applyKyselyEventsBatchFilters,
4
5
  applyMemoryDbSchema,
5
6
  applyWorkerDbSchema,
6
- broadcastChannelNames,
7
+ baseSystemMigrations,
8
+ compareHLC,
7
9
  createBroadcastChannels,
10
+ createCrdtApplyFunction,
11
+ createCrdtStorage,
12
+ createCrdtSyncProducer,
8
13
  createCrdtSyncRemoteSource,
9
- createSQLiteKysely,
10
- createSyncDbMigrations,
11
- createSyncDbMigrator,
14
+ createKvStoreTableQuery,
15
+ createMigrations,
16
+ createMigrator,
17
+ createSQLiteCrdtApplyFunction,
18
+ createSQLiteKvStore,
19
+ createStoredValue,
20
+ deserializeHLC,
21
+ dummyKysely,
12
22
  introspectDb,
13
- isWorkerInitMessage,
14
- isWorkerInitResponse,
23
+ isWorkerErrorResponseMessage,
15
24
  isWorkerNotificationMessage,
16
- isWorkerRequestMessage,
17
25
  isWorkerResponseMessage,
26
+ runSystemMigrations,
27
+ serializeHLC,
18
28
  startPerformanceLogger,
19
- syncDbWorkerLockName
20
- } from "./chunk-LK5FJCUD.js";
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
- orderBy,
38
- registerCrdtFunctions
39
- } from "./chunk-YLXMST5Z.js";
38
+ quoteId,
39
+ tryCatch,
40
+ tryCatchAsync
41
+ } from "./chunk-UGF5IU53.js";
40
42
 
41
- // src/hlc.ts
42
- var HLCCounter = class {
43
- timestamp;
44
- counter;
45
- nodeId;
46
- getTimestamp;
47
- constructor(nodeId, getTimestamp) {
48
- this.timestamp = getTimestamp();
49
- this.counter = 0;
50
- this.nodeId = nodeId;
51
- this.getTimestamp = getTimestamp;
52
- }
53
- getCurrentHLC() {
54
- return {
55
- timestamp: this.timestamp,
56
- counter: this.counter,
57
- nodeId: this.nodeId
58
- };
59
- }
60
- getNextHLC() {
61
- const now = this.getTimestamp();
62
- if (now > this.timestamp) {
63
- this.timestamp = now;
64
- this.counter = 0;
65
- return this.getCurrentHLC();
66
- }
67
- this.counter++;
68
- return this.getCurrentHLC();
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
- mergeHLC(hlc) {
71
- if (this.timestamp === hlc.timestamp) {
72
- this.counter = Math.max(this.counter, hlc.counter) + 1;
73
- } else if (this.timestamp > hlc.timestamp) {
74
- this.counter++;
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.timestamp = hlc.timestamp;
77
- this.counter = hlc.counter + 1;
61
+ this.map.set(key, value);
78
62
  }
79
- }
80
- };
81
- function serializeHLC(hlc) {
82
- return hlc.timestamp.toString().padStart(15, "0") + ":" + hlc.counter.toString(36).padStart(5, "0") + ":" + hlc.nodeId;
83
- }
84
- function deserializeHLC(serialized) {
85
- const [ts, count, ...node] = serialized.split(":");
86
- return {
87
- timestamp: parseInt(ts),
88
- counter: parseInt(count, 36),
89
- nodeId: node.join(":")
90
- };
91
- }
92
- function compareHLC(one, two) {
93
- if (one.timestamp == two.timestamp) {
94
- if (one.counter === two.counter) {
95
- if (one.nodeId === two.nodeId) {
96
- return 0;
97
- }
98
- return one.nodeId < two.nodeId ? -1 : 1;
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
- const handleWorkerResponse = (message) => {
125
- const promise = workerRequestsMap.get(message.requestId);
126
- if (!promise) {
127
- return;
128
- }
129
- promise.resolve(message.data);
130
- workerRequestsMap.delete(message.requestId);
68
+ get = (key) => {
69
+ return this.map.get(key);
131
70
  };
132
- broadcastChannels.responses.onmessage = (event) => {
133
- const message = event.data;
134
- if (isWorkerResponseMessage(message)) {
135
- handleWorkerResponse(message);
136
- } else if (isWorkerNotificationMessage(message)) {
137
- eventTarget.dispatchEvent("new-notification", message);
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
- const rpc = {
141
- execute: (query) => queryWorker("execute", [query]),
142
- getSnapshot: () => queryWorker("getSnapshot", []),
143
- pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
144
- pullEvents: (params) => queryWorker("pullEvents", [params]),
145
- postInitReady: () => queryWorker("postInitReady", [])
146
- };
147
- return {
148
- ...rpc,
149
- addEventListener: eventTarget.addEventListener,
150
- removeEventListener: eventTarget.removeEventListener
151
- };
152
- };
153
- function initializeWorkerDb({
154
- worker,
155
- broadcastChannels,
156
- config
157
- }) {
158
- const promise = createDeferredPromise();
159
- broadcastChannels.responses.onmessage = (event) => {
160
- const message = event.data;
161
- if (!isWorkerInitResponse(message)) {
162
- return;
80
+ clear = () => {
81
+ const onRemove = this.onRemove;
82
+ if (onRemove) {
83
+ this.map.forEach((value, key) => {
84
+ onRemove(key, value);
85
+ });
163
86
  }
164
- promise.resolve();
165
- worker.onmessage = null;
166
- };
167
- const configMessage = {
168
- type: "init",
169
- config
87
+ this.map.clear();
170
88
  };
171
- worker.postMessage(configMessage);
172
- broadcastChannels.requests.postMessage({
173
- type: "request",
174
- requestId: crypto.randomUUID(),
175
- method: "postInitReady",
176
- args: []
177
- });
178
- return promise.promise;
179
- }
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 ?? defaultLogger;
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", "info");
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 = () => this.db.execute({
236
- sql: query.sql,
237
- parameters: query.parameters ?? []
238
- }).rows;
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
- const refresh = () => {
248
- rows = fetchRows();
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: sql2, onDataChange } = params;
269
- const tables = this.getTablesUsed(sql2);
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 == 0 && query.toLowerCase().includes("delete")) {
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]}`).rows;
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/memory-db/memory-db.ts
432
- import { sql } from "kysely";
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
- db.execute(`
445
- create view ${crdtTableName} as
446
- select * from ${baseTableName}
447
- where tombstone = 0;`);
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) => "'{'||" + allColumnNames.map((col) => `'"${col}":'||json_quote(${from}.${col})`).join("||','||") + "||'}'";
450
- db.execute(`
451
- create trigger ${crdtTableName}_created
452
- instead of insert on ${crdtTableName}
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
- db.execute(`
459
- create trigger ${crdtTableName}_updated
460
- instead of update on ${crdtTableName}
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
- db.execute(`
471
- create trigger ${crdtTableName}_deleted
472
- instead of delete on ${crdtTableName}
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 = createSyncIdCounter({
499
- initialSyncId: 0
500
- });
501
- const pendingLocalEvents = [];
502
- registerCrdtFunctions({
503
- db,
504
- getTableSchema: (dataset) => db.dbSchema[dataset],
505
- getNextTimestamp: () => serializeHLC(hlcCounter.getNextHLC()),
506
- updateLogTableName: "crdt_update_log",
507
- onEventApplied: (event) => {
508
- const persistedEvent = {
509
- ...event,
510
- origin: tabId,
511
- sync_id: ++localSyncId.current,
512
- status: "applied"
513
- };
514
- enqueueCrdtEvent(db, persistedEvent);
515
- pendingLocalEvents.push(persistedEvent);
516
- }
517
- });
518
- db.createScalarFunction({
519
- name: "gen_id",
520
- callback: () => generateId(),
521
- deterministic: false,
522
- directOnly: false,
523
- innocuous: true
524
- });
525
- reactiveDb.addEventListener("transaction-rolled-back", () => {
526
- pendingLocalEvents.length = 0;
527
- });
528
- reactiveDb.addEventListener("transaction-committed", () => {
529
- const appliedEvents = pendingLocalEvents.splice(0);
530
- queueMicrotask(() => {
531
- for (const event of appliedEvents) {
532
- crdtStorage.dispatchEvent("event-applied", event);
533
- }
534
- crdtStorage.dispatchEvent("event-processing-done", void 0);
535
- });
624
+ const localSyncId = createStoredValue({
625
+ initialValue: 0
536
626
  });
537
627
  const crdtStorage = createCrdtStorage({
628
+ nodeId,
538
629
  syncId: localSyncId,
539
- persistEvents: (events) => persistEvents(db, events),
540
- popPendingEventsBatch: () => popPendingEventsBatch(db, 50),
541
- applyCrdtEventMutations: (event) => applyCrdtEventMutations({
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
- updateEventStatus: (syncId, status) => updateEventStatus(db, syncId, status)
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 enqueueCrdtEvent(db, event) {
649
+ function persistEvent(db, event) {
553
650
  db.executePrepared(
554
- "enqueue-crdt-events",
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
- origin: params("origin")
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 persistEvents(db, events) {
569
- db.executeTransaction((db2) => {
570
- const chunkSize = 100;
571
- for (let i = 0; i < events.length; i += chunkSize) {
572
- const chunk = events.slice(i, i + chunkSize);
573
- db2.executeKysely(
574
- (db3) => db3.insertInto("persisted_crdt_events").values(chunk)
575
- );
576
- }
577
- });
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 popPendingEventsBatch(db, limit) {
580
- const events = db.executePrepared(
581
- "pop-enqueued-crdt-events",
582
- {
583
- limit
584
- },
585
- (db2, param) => db2.selectFrom("persisted_crdt_events").where("status", "=", sql.lit("pending")).limit(param("limit")).orderBy("sync_id", "asc").selectAll()
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
- events,
589
- hasMore: events.length === limit
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 updateEventStatus(db, syncId, status) {
593
- db.executePrepared(
594
- "update-crdt-event-status",
595
- { syncId, status },
596
- (db2, params) => db2.updateTable("persisted_crdt_events").set({ status: params("status") }).where("sync_id", "=", params("syncId"))
597
- );
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
- async function createSyncedDb(options) {
602
- if (!options.dbPath.startsWith("/")) {
603
- throw new Error("dbPath must be an absolute path");
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
- await initializeWorkerDb({
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
- dbPath: options.dbPath,
831
+ dbId: options.dbId,
613
832
  clearOnInit: options.clearOnInit,
614
- syncServer: {
615
- host: "",
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
- tabId,
632
- crdtTables: options.crdtTables
862
+ crdtTables: options.syncDbSchema.tablesConfig
633
863
  });
634
- const remoteSyncId = createSyncIdCounter({
635
- initialSyncId: workerClientSnapshot.syncId
864
+ const pullSyncId = createStoredValue({
865
+ initialValue: workerClientSnapshot.syncId
636
866
  });
637
- const remoteSyncSource = createCrdtSyncRemoteSource({
638
- bufferSize: 100,
639
- syncId: remoteSyncId,
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
- pullEvents: (request) => workerClient.pullEvents(request),
643
- pushEvents: (request) => workerClient.pushTabEvents(request)
644
- });
645
- workerClient.addEventListener("new-notification", (event) => {
646
- const notification = event.payload;
647
- if (notification.notificationType === "new-event-chunk-applied" && notification.newSyncId > remoteSyncId.current) {
648
- remoteSyncSource.pullEvents();
649
- }
650
- });
651
- crdtStorage.addEventListener("event-applied", (event) => {
652
- if (event.payload.origin === "remote") {
653
- hlcCounter.mergeHLC(deserializeHLC(event.payload.timestamp));
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: reactiveDb.db,
658
- reactiveDb,
659
- workerDb: workerClient
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
- applyCrdtEventMutations,
934
+ applyKyselyEventsBatchFilters,
670
935
  applyMemoryDbSchema,
671
936
  applyWorkerDbSchema,
672
- broadcastChannelNames,
937
+ baseSystemMigrations,
673
938
  compareHLC,
674
- crdtSchema,
675
- createAsyncAutoFlushBuffer,
676
- createAutoFlushBuffer,
677
- createBroadcastChannels,
939
+ createCrdtApplyFunction,
678
940
  createCrdtStorage,
941
+ createCrdtStorageMutator,
679
942
  createCrdtSyncProducer,
680
943
  createCrdtSyncRemoteSource,
681
944
  createDeferredPromise,
682
- createMemoryDb,
683
- createSQLiteKysely,
684
- createSQLiteReactiveDb,
685
- createSyncDbMigrations,
686
- createSyncDbMigrator,
687
- createSyncIdCounter,
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
- orderBy,
705
- registerCrdtFunctions,
960
+ quoteId,
961
+ runSystemMigrations,
706
962
  serializeHLC,
707
963
  startPerformanceLogger,
708
- syncDbWorkerLockName
964
+ tryCatch,
965
+ tryCatchAsync
709
966
  };
710
967
  //# sourceMappingURL=index.js.map