@sqlite-sync/core 0.8.2 → 0.9.0

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,1330 +1,949 @@
1
- import {
2
- CRDT_EVENT_NO_OP_PAYLOAD,
3
- CrdtEventValidationError,
4
- HLCCounter,
5
- RESET_REQUEST_TTL_MS,
6
- SQLiteDbWrapper,
7
- applyMemoryDbSchema,
8
- applyWorkerDbSchema,
9
- baseSystemMigrations,
10
- compareHLC,
11
- createBroadcastChannels,
12
- createCrdtApplyFunction,
13
- createCrdtStorage,
14
- createCrdtSyncProducer,
15
- createCrdtSyncRemoteSource,
16
- createExportData,
17
- createImportData,
18
- createKvStoreTableQuery,
19
- createMigrations,
20
- createMigrator,
21
- createSQLiteCrdtApplyFunction,
22
- createSQLiteKvStore,
23
- createStoredValue,
24
- createSystemDbConfig,
25
- deserializeHLC,
26
- dummyKysely,
27
- formatSchemaVerificationIssues,
28
- introspectDb,
29
- isNoOpCrdtEventPayload,
30
- isWorkerErrorResponseMessage,
31
- isWorkerNotificationMessage,
32
- isWorkerResponseMessage,
33
- memoryDbConfig,
34
- runSystemMigrations,
35
- serializeHLC,
36
- startPerformanceLogger,
37
- syncDbClientLockName,
38
- validateNewCrdtEvent,
39
- verifySyncSchema,
40
- xxhash
41
- } from "./chunk-GMF45NFH.js";
42
- import {
43
- TypedBroadcastChannel,
44
- TypedEvent,
45
- createDeferredPromise,
46
- createTypedEventTarget,
47
- generateId,
48
- jsonSafeParse,
49
- noop,
50
- quoteId,
51
- tryCatch,
52
- tryCatchAsync
53
- } from "./chunk-NHT3ELMN.js";
54
-
55
- // src/memory-db/sqlite-reactive-db.ts
1
+ import { A as serializeHLC, C as createMigrator, D as HLCCounter, E as introspectDb, M as createExportData, N as createImportData, O as compareHLC, P as dummyKysely, S as createMigrations, T as startPerformanceLogger, _ as createSQLiteCrdtApplyFunction, a as createBroadcastChannels, b as CrdtEventValidationError, c as isWorkerNotificationMessage, d as syncDbClientLockName, g as createCrdtApplyFunction, h as createCrdtStorage, j as xxhash, k as deserializeHLC, m as createCrdtSyncProducer, o as isWorkerErrorResponseMessage, p as createCrdtSyncRemoteSource, t as RESET_REQUEST_TTL_MS, u as isWorkerResponseMessage, v as formatSchemaVerificationIssues, w as SQLiteDbWrapper, x as validateNewCrdtEvent, y as verifySyncSchema } from "./reset-state-D3nsfx_t.js";
2
+ import { c as noop, d as tryCatch, f as tryCatchAsync, i as createTypedEventTarget, n as TypedEvent, o as generateId, r as createDeferredPromise, s as jsonSafeParse, t as TypedBroadcastChannel, u as quoteId } from "./utils-Dr5JwcTP.js";
3
+ import { a as memoryDbConfig, c as createKvStoreTableQuery, d as CRDT_EVENT_NO_OP_PAYLOAD, f as isNoOpCrdtEventPayload, l as createSQLiteKvStore, n as applyWorkerDbSchema, t as applyMemoryDbSchema, u as createStoredValue } from "./system-schema-CkglcfTS.js";
4
+ import { a as registerCrdtIntentDrainer, i as makeCrdtTable } from "./make-crdt-table-BvsEnvu-.js";
56
5
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
57
-
58
- // src/bound-map.ts
6
+ //#region src/bound-map.ts
59
7
  var BoundMap = class {
60
- map = /* @__PURE__ */ new Map();
61
- maxSize;
62
- onRemove;
63
- constructor(opts) {
64
- this.maxSize = opts.maxSize;
65
- this.onRemove = opts.onRemove;
66
- }
67
- set = (key, value) => {
68
- if (this.onRemove && this.map.has(key)) {
69
- const old = this.map.get(key);
70
- this.map.set(key, value);
71
- this.onRemove(key, old);
72
- } else {
73
- this.map.set(key, value);
74
- }
75
- if (this.map.size > this.maxSize) {
76
- const firstKey = this.map.keys().next().value;
77
- this.delete(firstKey);
78
- }
79
- };
80
- get = (key) => {
81
- return this.map.get(key);
82
- };
83
- delete = (key) => {
84
- if (this.onRemove && this.map.has(key)) {
85
- const value = this.map.get(key);
86
- this.map.delete(key);
87
- this.onRemove(key, value);
88
- } else {
89
- this.map.delete(key);
90
- }
91
- };
92
- clear = () => {
93
- const onRemove = this.onRemove;
94
- if (onRemove) {
95
- this.map.forEach((value, key) => {
96
- onRemove(key, value);
97
- });
98
- }
99
- this.map.clear();
100
- };
8
+ map = /* @__PURE__ */ new Map();
9
+ maxSize;
10
+ onRemove;
11
+ constructor(opts) {
12
+ this.maxSize = opts.maxSize;
13
+ this.onRemove = opts.onRemove;
14
+ }
15
+ set = (key, value) => {
16
+ if (this.onRemove && this.map.has(key)) {
17
+ const old = this.map.get(key);
18
+ this.map.set(key, value);
19
+ this.onRemove(key, old);
20
+ } else this.map.set(key, value);
21
+ if (this.map.size > this.maxSize) {
22
+ const firstKey = this.map.keys().next().value;
23
+ this.delete(firstKey);
24
+ }
25
+ };
26
+ get = (key) => {
27
+ return this.map.get(key);
28
+ };
29
+ delete = (key) => {
30
+ if (this.onRemove && this.map.has(key)) {
31
+ const value = this.map.get(key);
32
+ this.map.delete(key);
33
+ this.onRemove(key, value);
34
+ } else this.map.delete(key);
35
+ };
36
+ clear = () => {
37
+ const onRemove = this.onRemove;
38
+ if (onRemove) this.map.forEach((value, key) => {
39
+ onRemove(key, value);
40
+ });
41
+ this.map.clear();
42
+ };
101
43
  };
102
-
103
- // src/memory-db/sqlite-reactive-db.ts
104
- var sqliteModule = null;
44
+ //#endregion
45
+ //#region src/memory-db/sqlite-reactive-db.ts
46
+ let sqliteModule = null;
105
47
  function createSQLiteReactiveDb(opts) {
106
- return SQLiteReactiveDb.create(opts);
48
+ return SQLiteReactiveDb.create(opts);
107
49
  }
108
- var SQLiteReactiveDb = class _SQLiteReactiveDb {
109
- db;
110
- sqlite3;
111
- logger;
112
- tablesUsedStatement = null;
113
- eventTarget = createTypedEventTarget();
114
- constructor(sqlite3, logger) {
115
- this.sqlite3 = sqlite3;
116
- this.logger = logger;
117
- this.db = new SQLiteDbWrapper({
118
- db: () => new sqlite3.oo1.DB({ filename: ":memory:" }),
119
- logger: this.logger,
120
- loggerPrefix: "memory",
121
- sqlite3
122
- });
123
- }
124
- static async create(opts) {
125
- const logger = opts.logger;
126
- const perf = startPerformanceLogger(logger);
127
- if (!sqliteModule) {
128
- sqliteModule = await sqlite3InitModule();
129
- }
130
- const db = new _SQLiteReactiveDb(sqliteModule, logger);
131
- if (opts.snapshot) {
132
- db.useSnapshot(opts.snapshot);
133
- }
134
- db.registerDbHooks();
135
- perf.logEnd("createSQLiteMemoryDb", "success", "system");
136
- return db;
137
- }
138
- liveQueryStatements = new BoundMap({
139
- maxSize: 100,
140
- onRemove(_, value) {
141
- value.finalize();
142
- }
143
- });
144
- sharedLiveQueries = /* @__PURE__ */ new Map();
145
- createLiveQuery(query) {
146
- const fetchRows = (parameters) => {
147
- let statement = this.liveQueryStatements.get(query.sql);
148
- if (!statement) {
149
- statement = this.db.prepare(query.sql);
150
- this.liveQueryStatements.set(query.sql, statement);
151
- }
152
- return statement.execute(parameters);
153
- };
154
- let rows = null;
155
- const getRows = () => {
156
- if (!rows) {
157
- rows = fetchRows(query.parameters);
158
- }
159
- return rows;
160
- };
161
- let subscriber = null;
162
- let lastParameters = query.parameters;
163
- const refresh = (parameters) => {
164
- if (parameters) {
165
- lastParameters = parameters;
166
- }
167
- rows = fetchRows(lastParameters);
168
- subscriber?.();
169
- };
170
- const subscribe = (onchange) => {
171
- if (subscriber) {
172
- throw new Error("Subscriber already exists");
173
- }
174
- subscriber = onchange;
175
- const subscription = this.subscribeToQueryChanges({
176
- sql: query.sql,
177
- onDataChange: refresh
178
- });
179
- return () => {
180
- subscription.unsubscribe();
181
- subscriber = null;
182
- };
183
- };
184
- return { getRows, refresh, subscribe };
185
- }
186
- getSharedLiveQuery(query) {
187
- const existingEntry = this.sharedLiveQueries.get(query.sql)?.find((entry2) => parametersAreEqual(entry2.parameters, query.parameters));
188
- if (existingEntry) {
189
- if (existingEntry.listeners.size === 0) {
190
- this.scheduleSharedLiveQueryCleanup(existingEntry);
191
- }
192
- return existingEntry;
193
- }
194
- const liveQuery = this.createLiveQuery(query);
195
- let hasDetachedFromLiveQuery = false;
196
- const entry = {
197
- sql: query.sql,
198
- parameters: query.parameters,
199
- listeners: /* @__PURE__ */ new Set(),
200
- unsubscribeFromLiveQuery: null,
201
- cleanupTimeout: null,
202
- getRows: () => liveQuery.getRows(),
203
- refresh: () => {
204
- liveQuery.refresh();
205
- },
206
- getSubscriberCount: () => entry.listeners.size,
207
- subscribe: (onchange) => {
208
- this.cancelSharedLiveQueryCleanup(entry);
209
- this.registerSharedLiveQueryEntry(entry);
210
- entry.listeners.add(onchange);
211
- if (!entry.unsubscribeFromLiveQuery) {
212
- entry.unsubscribeFromLiveQuery = liveQuery.subscribe(() => {
213
- for (const listener of entry.listeners) {
214
- listener();
215
- }
216
- });
217
- if (hasDetachedFromLiveQuery) {
218
- hasDetachedFromLiveQuery = false;
219
- liveQuery.refresh();
220
- }
221
- }
222
- return () => {
223
- entry.listeners.delete(onchange);
224
- if (entry.listeners.size === 0) {
225
- entry.unsubscribeFromLiveQuery?.();
226
- entry.unsubscribeFromLiveQuery = null;
227
- hasDetachedFromLiveQuery = true;
228
- this.scheduleSharedLiveQueryCleanup(entry);
229
- }
230
- };
231
- }
232
- };
233
- this.registerSharedLiveQueryEntry(entry);
234
- this.scheduleSharedLiveQueryCleanup(entry);
235
- return entry;
236
- }
237
- registerSharedLiveQueryEntry(entry) {
238
- const matchingSqlEntries = this.sharedLiveQueries.get(entry.sql) ?? [];
239
- if (!matchingSqlEntries.includes(entry)) {
240
- matchingSqlEntries.push(entry);
241
- this.sharedLiveQueries.set(entry.sql, matchingSqlEntries);
242
- }
243
- }
244
- getSharedLiveQueriesSnapshot() {
245
- const snapshot = [];
246
- for (const entries of this.sharedLiveQueries.values()) {
247
- for (const entry of entries) {
248
- snapshot.push({
249
- sql: entry.sql,
250
- parameters: entry.parameters,
251
- subscriberCount: entry.listeners.size
252
- });
253
- }
254
- }
255
- return snapshot;
256
- }
257
- scheduleSharedLiveQueryCleanup(entry) {
258
- this.cancelSharedLiveQueryCleanup(entry);
259
- entry.cleanupTimeout = setTimeout(() => {
260
- entry.cleanupTimeout = null;
261
- this.releaseSharedLiveQuery(entry);
262
- }, 0);
263
- }
264
- cancelSharedLiveQueryCleanup(entry) {
265
- if (entry.cleanupTimeout) {
266
- clearTimeout(entry.cleanupTimeout);
267
- entry.cleanupTimeout = null;
268
- }
269
- }
270
- releaseSharedLiveQuery(entry) {
271
- if (entry.listeners.size > 0 || entry.unsubscribeFromLiveQuery) {
272
- return;
273
- }
274
- const matchingSqlEntries = this.sharedLiveQueries.get(entry.sql);
275
- if (!matchingSqlEntries) {
276
- return;
277
- }
278
- const nextEntries = matchingSqlEntries.filter((candidate) => candidate !== entry);
279
- if (nextEntries.length > 0) {
280
- this.sharedLiveQueries.set(entry.sql, nextEntries);
281
- } else {
282
- this.sharedLiveQueries.delete(entry.sql);
283
- }
284
- }
285
- subscribeToQueryChanges(params) {
286
- const { sql, onDataChange } = params;
287
- const tables = this.getTablesUsed(sql);
288
- const readTables = /* @__PURE__ */ new Set();
289
- for (const table of tables) {
290
- if (!readTables.has(table.name)) {
291
- readTables.add(table.name);
292
- } else if (table.isWrite) {
293
- throw new Error("This query writes and reads from the same table. This may cause infinite loops.");
294
- }
295
- }
296
- const notifyDataChange = createDebouncedCallback(() => {
297
- onDataChange();
298
- }, 30);
299
- for (const table of readTables) {
300
- this.eventTarget.addEventListener(`table:${table}`, notifyDataChange);
301
- }
302
- this.eventTarget.addEventListener("any-table-changed", notifyDataChange);
303
- return {
304
- unsubscribe: () => {
305
- for (const table of readTables) {
306
- this.eventTarget.removeEventListener(`table:${table}`, notifyDataChange);
307
- }
308
- this.eventTarget.removeEventListener("any-table-changed", notifyDataChange);
309
- }
310
- };
311
- }
312
- subscribeToTableChanges(table, onChanges) {
313
- this.eventTarget.addEventListener(`table:${table}`, onChanges);
314
- this.eventTarget.addEventListener("any-table-changed", onChanges);
315
- return {
316
- unsubscribe: () => {
317
- this.eventTarget.removeEventListener(`table:${table}`, onChanges);
318
- this.eventTarget.removeEventListener("any-table-changed", onChanges);
319
- }
320
- };
321
- }
322
- getTablesUsed(query) {
323
- if (!this.tablesUsedStatement) {
324
- this.tablesUsedStatement = this.db.prepare(
325
- "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'",
326
- { loggerLevel: "system" }
327
- );
328
- }
329
- const tables = this.tablesUsedStatement.execute([query]);
330
- if (tables.length === 0 && query.toLowerCase().includes("delete")) {
331
- tables.push(...this.getClearedTables(query));
332
- }
333
- return tables;
334
- }
335
- getClearedTables(query) {
336
- const operations = this.db.execute(`EXPLAIN ${query.split(";")[0]}`, { loggerLevel: "system" }).rows;
337
- const clearedTablesRootPages = /* @__PURE__ */ new Set();
338
- for (const operation of operations) {
339
- if (operation.opcode === "Clear" && operation.p2 === 0) {
340
- clearedTablesRootPages.add(operation.p1);
341
- }
342
- }
343
- if (clearedTablesRootPages.size === 0) {
344
- return [];
345
- }
346
- const tableNames = this.db.execute(
347
- `select t.tbl_name as name, true as isWrite from sqlite_master as t where t.rootpage in (${Array.from(
348
- clearedTablesRootPages
349
- ).join(",")})`,
350
- { loggerLevel: "system" }
351
- ).rows;
352
- return tableNames;
353
- }
354
- addEventListener(type, listener) {
355
- this.eventTarget.addEventListener(type, listener);
356
- }
357
- removeEventListener(type, listener) {
358
- this.eventTarget.removeEventListener(type, listener);
359
- }
360
- notifyTableSubscribers(tables = null) {
361
- if (!tables) {
362
- this.eventTarget.dispatchEvent("any-table-changed", void 0);
363
- return;
364
- }
365
- for (const table of tables) {
366
- this.eventTarget.dispatchEvent(`table:${table}`, void 0);
367
- }
368
- }
369
- registerDbHooks() {
370
- let updateQueue = /* @__PURE__ */ new Set();
371
- this.sqlite3.capi.sqlite3_update_hook(
372
- this.db.ensureDb,
373
- (_ctx, _opId, _db, table) => {
374
- updateQueue.add(table);
375
- },
376
- 0
377
- );
378
- this.sqlite3.capi.sqlite3_rollback_hook(
379
- this.db.ensureDb,
380
- () => {
381
- if (updateQueue.size === 0) {
382
- return 0;
383
- }
384
- updateQueue.clear();
385
- this.eventTarget.dispatchEvent("transaction-rolled-back", void 0);
386
- return 0;
387
- },
388
- 0
389
- );
390
- this.sqlite3.capi.sqlite3_commit_hook(
391
- this.db.ensureDb,
392
- () => {
393
- if (updateQueue.size === 0) {
394
- return 0;
395
- }
396
- const tables = updateQueue;
397
- updateQueue = /* @__PURE__ */ new Set();
398
- this.eventTarget.dispatchEvent("transaction-committed", void 0);
399
- queueMicrotask(() => {
400
- this.notifyTableSubscribers(tables);
401
- });
402
- return 0;
403
- },
404
- 0
405
- );
406
- }
407
- createSnapshot() {
408
- const perf = startPerformanceLogger(this.logger);
409
- const snapshot = this.sqlite3.capi.sqlite3_js_db_export(this.db.ensureDb);
410
- perf.logEnd("createSnapshot", `snapshot size: ${snapshot.byteLength}`, "info");
411
- return snapshot;
412
- }
413
- useSnapshot(snapshot) {
414
- this.db.useSnapshot(snapshot);
415
- this.notifyTableSubscribers();
416
- }
417
- dispose() {
418
- for (const entries of this.sharedLiveQueries.values()) {
419
- for (const entry of entries) {
420
- this.cancelSharedLiveQueryCleanup(entry);
421
- entry.unsubscribeFromLiveQuery?.();
422
- entry.unsubscribeFromLiveQuery = null;
423
- entry.listeners.clear();
424
- }
425
- }
426
- this.sharedLiveQueries.clear();
427
- this.liveQueryStatements.clear();
428
- if (this.tablesUsedStatement) {
429
- this.tablesUsedStatement.finalize();
430
- this.tablesUsedStatement = null;
431
- }
432
- this.db.close();
433
- }
50
+ var SQLiteReactiveDb = class SQLiteReactiveDb {
51
+ db;
52
+ sqlite3;
53
+ logger;
54
+ tablesUsedStatement = null;
55
+ eventTarget = createTypedEventTarget();
56
+ mutationVersion = 0;
57
+ tableMutationVersions = /* @__PURE__ */ new Map();
58
+ constructor(sqlite3, logger) {
59
+ this.sqlite3 = sqlite3;
60
+ this.logger = logger;
61
+ this.db = new SQLiteDbWrapper({
62
+ db: () => new sqlite3.oo1.DB({ filename: ":memory:" }),
63
+ logger: this.logger,
64
+ loggerPrefix: "memory",
65
+ sqlite3
66
+ });
67
+ }
68
+ static async create(opts) {
69
+ const logger = opts.logger;
70
+ const perf = startPerformanceLogger(logger, "system");
71
+ if (!sqliteModule) sqliteModule = await sqlite3InitModule();
72
+ const db = new SQLiteReactiveDb(sqliteModule, logger);
73
+ if (opts.snapshot) db.useSnapshot(opts.snapshot);
74
+ db.registerDbHooks();
75
+ perf.logEnd("createSQLiteMemoryDb", "success", "system");
76
+ return db;
77
+ }
78
+ liveQueryStatements = new BoundMap({
79
+ maxSize: 100,
80
+ onRemove(_, value) {
81
+ value.finalize();
82
+ }
83
+ });
84
+ sharedLiveQueries = /* @__PURE__ */ new Map();
85
+ createLiveQuery(query) {
86
+ const fetchRows = (parameters) => {
87
+ let statement = this.liveQueryStatements.get(query.sql);
88
+ if (!statement) {
89
+ statement = this.db.prepare(query.sql);
90
+ this.liveQueryStatements.set(query.sql, statement);
91
+ }
92
+ return statement.execute(parameters);
93
+ };
94
+ let rows = null;
95
+ const getRows = () => {
96
+ if (!rows) rows = fetchRows(query.parameters);
97
+ return rows;
98
+ };
99
+ let subscriber = null;
100
+ let lastParameters = query.parameters;
101
+ const refresh = (parameters) => {
102
+ if (parameters) lastParameters = parameters;
103
+ rows = fetchRows(lastParameters);
104
+ subscriber?.();
105
+ };
106
+ const subscribe = (onchange) => {
107
+ if (subscriber) throw new Error("Subscriber already exists");
108
+ subscriber = onchange;
109
+ const subscription = this.subscribeToQueryChanges({
110
+ sql: query.sql,
111
+ onDataChange: refresh
112
+ });
113
+ return () => {
114
+ subscription.unsubscribe();
115
+ subscriber = null;
116
+ };
117
+ };
118
+ return {
119
+ getRows,
120
+ refresh,
121
+ subscribe
122
+ };
123
+ }
124
+ getSharedLiveQuery(query) {
125
+ const existingEntry = this.sharedLiveQueries.get(query.sql)?.find((entry) => parametersAreEqual(entry.parameters, query.parameters));
126
+ if (existingEntry) {
127
+ if (existingEntry.listeners.size === 0) this.scheduleSharedLiveQueryCleanup(existingEntry);
128
+ return existingEntry;
129
+ }
130
+ const liveQuery = this.createLiveQuery(query);
131
+ let hasDetachedFromLiveQuery = false;
132
+ const entry = {
133
+ sql: query.sql,
134
+ parameters: query.parameters,
135
+ listeners: /* @__PURE__ */ new Set(),
136
+ unsubscribeFromLiveQuery: null,
137
+ cleanupTimeout: null,
138
+ getRows: () => liveQuery.getRows(),
139
+ refresh: () => {
140
+ liveQuery.refresh();
141
+ },
142
+ getSubscriberCount: () => entry.listeners.size,
143
+ subscribe: (onchange) => {
144
+ this.cancelSharedLiveQueryCleanup(entry);
145
+ this.registerSharedLiveQueryEntry(entry);
146
+ entry.listeners.add(onchange);
147
+ if (!entry.unsubscribeFromLiveQuery) {
148
+ entry.unsubscribeFromLiveQuery = liveQuery.subscribe(() => {
149
+ for (const listener of entry.listeners) listener();
150
+ });
151
+ if (hasDetachedFromLiveQuery) {
152
+ hasDetachedFromLiveQuery = false;
153
+ liveQuery.refresh();
154
+ }
155
+ }
156
+ return () => {
157
+ entry.listeners.delete(onchange);
158
+ if (entry.listeners.size === 0) {
159
+ entry.unsubscribeFromLiveQuery?.();
160
+ entry.unsubscribeFromLiveQuery = null;
161
+ hasDetachedFromLiveQuery = true;
162
+ this.scheduleSharedLiveQueryCleanup(entry);
163
+ }
164
+ };
165
+ }
166
+ };
167
+ this.registerSharedLiveQueryEntry(entry);
168
+ this.scheduleSharedLiveQueryCleanup(entry);
169
+ return entry;
170
+ }
171
+ registerSharedLiveQueryEntry(entry) {
172
+ const matchingSqlEntries = this.sharedLiveQueries.get(entry.sql) ?? [];
173
+ if (!matchingSqlEntries.includes(entry)) {
174
+ matchingSqlEntries.push(entry);
175
+ this.sharedLiveQueries.set(entry.sql, matchingSqlEntries);
176
+ }
177
+ }
178
+ getSharedLiveQueriesSnapshot() {
179
+ const snapshot = [];
180
+ for (const entries of this.sharedLiveQueries.values()) for (const entry of entries) snapshot.push({
181
+ sql: entry.sql,
182
+ parameters: entry.parameters,
183
+ subscriberCount: entry.listeners.size
184
+ });
185
+ return snapshot;
186
+ }
187
+ scheduleSharedLiveQueryCleanup(entry) {
188
+ this.cancelSharedLiveQueryCleanup(entry);
189
+ entry.cleanupTimeout = setTimeout(() => {
190
+ entry.cleanupTimeout = null;
191
+ this.releaseSharedLiveQuery(entry);
192
+ }, 0);
193
+ }
194
+ cancelSharedLiveQueryCleanup(entry) {
195
+ if (entry.cleanupTimeout) {
196
+ clearTimeout(entry.cleanupTimeout);
197
+ entry.cleanupTimeout = null;
198
+ }
199
+ }
200
+ releaseSharedLiveQuery(entry) {
201
+ if (entry.listeners.size > 0 || entry.unsubscribeFromLiveQuery) return;
202
+ const matchingSqlEntries = this.sharedLiveQueries.get(entry.sql);
203
+ if (!matchingSqlEntries) return;
204
+ const nextEntries = matchingSqlEntries.filter((candidate) => candidate !== entry);
205
+ if (nextEntries.length > 0) this.sharedLiveQueries.set(entry.sql, nextEntries);
206
+ else this.sharedLiveQueries.delete(entry.sql);
207
+ }
208
+ subscribeToQueryChanges(params) {
209
+ const { sql, onDataChange } = params;
210
+ const tables = this.getTablesUsed(sql);
211
+ const readTables = /* @__PURE__ */ new Set();
212
+ for (const table of tables) if (!readTables.has(table.name)) readTables.add(table.name);
213
+ else if (table.isWrite) throw new Error("This query writes and reads from the same table. This may cause infinite loops.");
214
+ const notifyDataChange = createDebouncedCallback(() => {
215
+ onDataChange();
216
+ }, 30);
217
+ for (const table of readTables) this.eventTarget.addEventListener(`table:${table}`, notifyDataChange);
218
+ this.eventTarget.addEventListener("any-table-changed", notifyDataChange);
219
+ return { unsubscribe: () => {
220
+ for (const table of readTables) this.eventTarget.removeEventListener(`table:${table}`, notifyDataChange);
221
+ this.eventTarget.removeEventListener("any-table-changed", notifyDataChange);
222
+ } };
223
+ }
224
+ subscribeToTableChanges(table, onChanges) {
225
+ this.eventTarget.addEventListener(`table:${table}`, onChanges);
226
+ this.eventTarget.addEventListener("any-table-changed", onChanges);
227
+ return { unsubscribe: () => {
228
+ this.eventTarget.removeEventListener(`table:${table}`, onChanges);
229
+ this.eventTarget.removeEventListener("any-table-changed", onChanges);
230
+ } };
231
+ }
232
+ getTablesUsed(query) {
233
+ if (!this.tablesUsedStatement) this.tablesUsedStatement = this.db.prepare("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'", { loggerLevel: "system" });
234
+ const tables = this.tablesUsedStatement.execute([query]);
235
+ if (tables.length === 0 && query.toLowerCase().includes("delete")) tables.push(...this.getClearedTables(query));
236
+ return tables;
237
+ }
238
+ getClearedTables(query) {
239
+ const operations = this.db.execute(`EXPLAIN ${query.split(";")[0]}`, { loggerLevel: "system" }).rows;
240
+ const clearedTablesRootPages = /* @__PURE__ */ new Set();
241
+ for (const operation of operations) if (operation.opcode === "Clear" && operation.p2 === 0) clearedTablesRootPages.add(operation.p1);
242
+ if (clearedTablesRootPages.size === 0) return [];
243
+ return this.db.execute(`select t.tbl_name as name, true as isWrite from sqlite_master as t where t.rootpage in (${Array.from(clearedTablesRootPages).join(",")})`, { loggerLevel: "system" }).rows;
244
+ }
245
+ addEventListener(type, listener) {
246
+ this.eventTarget.addEventListener(type, listener);
247
+ }
248
+ removeEventListener(type, listener) {
249
+ this.eventTarget.removeEventListener(type, listener);
250
+ }
251
+ getTableMutationVersion(table) {
252
+ return this.tableMutationVersions.get(table) ?? 0;
253
+ }
254
+ notifyTableSubscribers(tables = null) {
255
+ if (!tables) {
256
+ this.eventTarget.dispatchEvent("any-table-changed", void 0);
257
+ return;
258
+ }
259
+ for (const table of tables) this.eventTarget.dispatchEvent(`table:${table}`, void 0);
260
+ }
261
+ registerDbHooks() {
262
+ let updateQueue = /* @__PURE__ */ new Set();
263
+ this.sqlite3.capi.sqlite3_update_hook(this.db.ensureDb, (_ctx, _opId, _db, table) => {
264
+ updateQueue.add(table);
265
+ this.tableMutationVersions.set(table, ++this.mutationVersion);
266
+ }, 0);
267
+ this.sqlite3.capi.sqlite3_rollback_hook(this.db.ensureDb, () => {
268
+ if (updateQueue.size === 0) return 0;
269
+ updateQueue.clear();
270
+ this.eventTarget.dispatchEvent("transaction-rolled-back", void 0);
271
+ return 0;
272
+ }, 0);
273
+ this.sqlite3.capi.sqlite3_commit_hook(this.db.ensureDb, () => {
274
+ if (updateQueue.size === 0) return 0;
275
+ const tables = updateQueue;
276
+ updateQueue = /* @__PURE__ */ new Set();
277
+ this.eventTarget.dispatchEvent("transaction-committed", void 0);
278
+ queueMicrotask(() => {
279
+ this.notifyTableSubscribers(tables);
280
+ });
281
+ return 0;
282
+ }, 0);
283
+ }
284
+ createSnapshot() {
285
+ const perf = startPerformanceLogger(this.logger);
286
+ const snapshot = this.sqlite3.capi.sqlite3_js_db_export(this.db.ensureDb);
287
+ perf.logEnd("createSnapshot", `snapshot size: ${snapshot.byteLength}`, "info");
288
+ return snapshot;
289
+ }
290
+ useSnapshot(snapshot) {
291
+ this.db.useSnapshot(snapshot);
292
+ this.notifyTableSubscribers();
293
+ }
294
+ dispose() {
295
+ for (const entries of this.sharedLiveQueries.values()) for (const entry of entries) {
296
+ this.cancelSharedLiveQueryCleanup(entry);
297
+ entry.unsubscribeFromLiveQuery?.();
298
+ entry.unsubscribeFromLiveQuery = null;
299
+ entry.listeners.clear();
300
+ }
301
+ this.sharedLiveQueries.clear();
302
+ this.liveQueryStatements.clear();
303
+ if (this.tablesUsedStatement) {
304
+ this.tablesUsedStatement.finalize();
305
+ this.tablesUsedStatement = null;
306
+ }
307
+ this.db.close();
308
+ }
434
309
  };
435
310
  function createDebouncedCallback(callback, delay) {
436
- let timeout = null;
437
- let shouldCallWithoutDelay = true;
438
- return (...args) => {
439
- if (shouldCallWithoutDelay) {
440
- callback(...args);
441
- shouldCallWithoutDelay = false;
442
- return;
443
- }
444
- const effect = () => {
445
- timeout = null;
446
- shouldCallWithoutDelay = true;
447
- return callback(...args);
448
- };
449
- if (timeout) {
450
- clearTimeout(timeout);
451
- }
452
- timeout = setTimeout(effect, delay);
453
- };
311
+ let timeout = null;
312
+ let shouldCallWithoutDelay = true;
313
+ return (...args) => {
314
+ if (shouldCallWithoutDelay) {
315
+ callback(...args);
316
+ shouldCallWithoutDelay = false;
317
+ return;
318
+ }
319
+ const effect = () => {
320
+ timeout = null;
321
+ shouldCallWithoutDelay = true;
322
+ return callback(...args);
323
+ };
324
+ if (timeout) clearTimeout(timeout);
325
+ timeout = setTimeout(effect, delay);
326
+ };
454
327
  }
455
328
  function parametersAreEqual(a, b) {
456
- if (a === b) return true;
457
- if (!a || !b) return false;
458
- if (a.length !== b.length) return false;
459
- for (let i = 0; i < a.length; i++) {
460
- if (!Object.is(a[i], b[i])) {
461
- return false;
462
- }
463
- }
464
- return true;
329
+ if (a === b) return true;
330
+ if (!a || !b) return false;
331
+ if (a.length !== b.length) return false;
332
+ for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
333
+ return true;
465
334
  }
466
-
467
- // src/schema/define-sync-schema.ts
468
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
469
- "change_history",
470
- "crdt_events",
471
- "worker.crdt_events",
472
- "persisted_crdt_events",
473
- "crdt_update_log",
474
- "worker.kv"
335
+ //#endregion
336
+ //#region src/schema/define-sync-schema.ts
337
+ const RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
338
+ "change_history",
339
+ "crdt_events",
340
+ "worker.crdt_events",
341
+ "persisted_crdt_events",
342
+ "crdt_update_log",
343
+ "crdt_change_intents",
344
+ "worker.kv"
475
345
  ]);
476
- function defineSyncSchema({
477
- tables,
478
- migrations
479
- }) {
480
- const tablesConfig = Object.entries(tables).map(([crdtTableName, table]) => ({
481
- crdtTableName,
482
- baseTableName: table.baseName ?? `_${crdtTableName}`
483
- }));
484
- const seenNames = /* @__PURE__ */ new Set();
485
- for (const { crdtTableName, baseTableName } of tablesConfig) {
486
- for (const name of [crdtTableName, baseTableName]) {
487
- if (RESERVED_TABLE_NAMES.has(name)) {
488
- throw new Error(`Table name "${name}" is reserved by sqlite-sync and cannot be used in a sync schema`);
489
- }
490
- if (seenNames.has(name)) {
491
- throw new Error(`Duplicate table name "${name}" in sync schema`);
492
- }
493
- seenNames.add(name);
494
- }
495
- }
496
- return {
497
- tables,
498
- get tablesConfig() {
499
- return tablesConfig;
500
- },
501
- get migrations() {
502
- return migrations;
503
- },
504
- get "~clientSchema"() {
505
- throw new Error("~clientSchema is type-only and cannot be accessed at runtime");
506
- },
507
- get "~serverSchema"() {
508
- throw new Error("~serverSchema is type-only and cannot be accessed at runtime");
509
- },
510
- get "~mutationsSchema"() {
511
- throw new Error("~mutationsSchema is type-only and cannot be accessed at runtime");
512
- }
513
- };
346
+ /**
347
+ * Define a sync database schema from `t.table()` builders.
348
+ *
349
+ * Record keys are the crdt (view) table names; base table names default to the key
350
+ * prefixed with "_", overridable per table via `t.table(cols, { baseName })`.
351
+ */
352
+ function defineSyncSchema({ tables, migrations }) {
353
+ const tablesConfig = Object.entries(tables).map(([crdtTableName, table]) => ({
354
+ crdtTableName,
355
+ baseTableName: table.baseName ?? `_${crdtTableName}`
356
+ }));
357
+ const seenNames = /* @__PURE__ */ new Set();
358
+ for (const { crdtTableName, baseTableName } of tablesConfig) for (const name of [crdtTableName, baseTableName]) {
359
+ const normalizedName = name.toLowerCase();
360
+ if (RESERVED_TABLE_NAMES.has(normalizedName)) throw new Error(`Table name "${name}" is reserved by sqlite-sync and cannot be used in a sync schema`);
361
+ if (seenNames.has(normalizedName)) throw new Error(`Duplicate table name "${name}" in sync schema`);
362
+ seenNames.add(normalizedName);
363
+ }
364
+ return {
365
+ tables,
366
+ get tablesConfig() {
367
+ return tablesConfig;
368
+ },
369
+ get migrations() {
370
+ return migrations;
371
+ },
372
+ get "~clientSchema"() {
373
+ throw new Error("~clientSchema is type-only and cannot be accessed at runtime");
374
+ },
375
+ get "~serverSchema"() {
376
+ throw new Error("~serverSchema is type-only and cannot be accessed at runtime");
377
+ },
378
+ get "~mutationsSchema"() {
379
+ throw new Error("~mutationsSchema is type-only and cannot be accessed at runtime");
380
+ }
381
+ };
514
382
  }
515
-
516
- // src/schema/table-builder.ts
517
- var ColumnBuilder = class _ColumnBuilder {
518
- constructor(meta) {
519
- this.meta = meta;
520
- }
521
- /** Allow SQL NULL; widens the value type with `null`. */
522
- nullable() {
523
- return new _ColumnBuilder({ ...this.meta, nullable: true });
524
- }
525
- /** Declare a column default. The value is used both in DDL and when migrating older events. */
526
- default(value) {
527
- return new _ColumnBuilder({ ...this.meta, hasDefault: true, defaultValue: value });
528
- }
529
- /** Narrow the TypeScript value type without changing storage, e.g. `t.text().$type<"movie" | "tv">()`. */
530
- $type() {
531
- return this;
532
- }
533
- /** Human/AI-readable description, surfaced in generated schema docs. */
534
- describe(description) {
535
- return new _ColumnBuilder({ ...this.meta, description });
536
- }
383
+ //#endregion
384
+ //#region src/schema/table-builder.ts
385
+ var ColumnBuilder = class ColumnBuilder {
386
+ meta;
387
+ constructor(meta) {
388
+ this.meta = meta;
389
+ }
390
+ /** Allow SQL NULL; widens the value type with `null`. */
391
+ nullable() {
392
+ return new ColumnBuilder({
393
+ ...this.meta,
394
+ nullable: true
395
+ });
396
+ }
397
+ /** Declare a column default. The value is used both in DDL and when migrating older events. */
398
+ default(value) {
399
+ return new ColumnBuilder({
400
+ ...this.meta,
401
+ hasDefault: true,
402
+ defaultValue: value
403
+ });
404
+ }
405
+ /** Narrow the TypeScript value type without changing storage, e.g. `t.text().$type<"movie" | "tv">()`. */
406
+ $type() {
407
+ return this;
408
+ }
409
+ /** Human/AI-readable description, surfaced in generated schema docs. */
410
+ describe(description) {
411
+ return new ColumnBuilder({
412
+ ...this.meta,
413
+ description
414
+ });
415
+ }
537
416
  };
538
- var RESERVED_COLUMNS = ["id", "tombstone"];
539
- var idColumnMeta = {
540
- kind: "text",
541
- sqlType: "text",
542
- nullable: false,
543
- hasDefault: false,
544
- description: "Unique immutable item id"
417
+ const RESERVED_COLUMNS = ["id", "tombstone"];
418
+ const idColumnMeta = {
419
+ kind: "text",
420
+ sqlType: "text",
421
+ nullable: false,
422
+ hasDefault: false,
423
+ description: "Unique immutable item id"
545
424
  };
546
- var tombstoneColumnMeta = {
547
- kind: "boolean",
548
- sqlType: "integer",
549
- nullable: false,
550
- hasDefault: true,
551
- defaultValue: false,
552
- description: "Soft-delete marker"
425
+ const tombstoneColumnMeta = {
426
+ kind: "boolean",
427
+ sqlType: "integer",
428
+ nullable: false,
429
+ hasDefault: true,
430
+ defaultValue: false,
431
+ description: "Soft-delete marker"
553
432
  };
554
- var TableBuilder = class _TableBuilder {
555
- constructor(userColumns, options) {
556
- this.userColumns = userColumns;
557
- for (const reserved of RESERVED_COLUMNS) {
558
- if (reserved in userColumns) {
559
- throw new Error(`Column "${reserved}" is added automatically to every synced table and cannot be declared`);
560
- }
561
- }
562
- this.columns = {
563
- id: idColumnMeta,
564
- ...Object.fromEntries(Object.entries(userColumns).map(([name, column2]) => [name, column2.meta])),
565
- tombstone: tombstoneColumnMeta
566
- };
567
- this.baseName = options?.baseName;
568
- this.description = options?.description;
569
- }
570
- /** All columns in DDL order, including the auto-injected `id` and `tombstone`. */
571
- columns;
572
- baseName;
573
- description;
574
- describe(description) {
575
- return new _TableBuilder(this.userColumns, { baseName: this.baseName, description });
576
- }
577
- /** Type-only: the row shape returned by queries. Do not access at runtime. */
578
- get $row() {
579
- return null;
580
- }
581
- /**
582
- * Validate a CRDT event payload against the declared columns.
583
- * `item-created` payloads must contain every column without a default (nullable columns may be omitted);
584
- * `item-updated` payloads are partial and must not touch `id` or set `tombstone` to a deleted value.
585
- */
586
- validatePayload(payload, options) {
587
- const event = options?.event ?? "item-created";
588
- const errors = [];
589
- for (const key of Object.keys(payload)) {
590
- if (!(key in this.columns)) {
591
- errors.push(`Unknown column "${key}"`);
592
- }
593
- }
594
- if (event === "item-updated") {
595
- if ("id" in payload) {
596
- errors.push(`Column "id" is immutable and cannot appear in an update payload`);
597
- }
598
- if (payload.tombstone === true || payload.tombstone === 1) {
599
- errors.push(`Column "tombstone" cannot be set to true/1 in an update payload; use an item-deleted event`);
600
- }
601
- }
602
- if (event === "item-created") {
603
- for (const [name, meta] of Object.entries(this.columns)) {
604
- if (name in payload || name === "tombstone" || meta.nullable || meta.hasDefault) {
605
- continue;
606
- }
607
- errors.push(`Missing required column "${name}"`);
608
- }
609
- }
610
- for (const [name, value] of Object.entries(payload)) {
611
- const meta = this.columns[name];
612
- if (!meta || event === "item-updated" && name === "id") {
613
- continue;
614
- }
615
- const error = validateValue(name, value, meta);
616
- if (error) {
617
- errors.push(error);
618
- }
619
- }
620
- return errors.length === 0 ? { success: true } : { success: false, errors };
621
- }
433
+ var TableBuilder = class TableBuilder {
434
+ userColumns;
435
+ /** All columns in DDL order, including the auto-injected `id` and `tombstone`. */
436
+ columns;
437
+ baseName;
438
+ description;
439
+ constructor(userColumns, options) {
440
+ this.userColumns = userColumns;
441
+ for (const reserved of RESERVED_COLUMNS) if (reserved in userColumns) throw new Error(`Column "${reserved}" is added automatically to every synced table and cannot be declared`);
442
+ this.columns = {
443
+ id: idColumnMeta,
444
+ ...Object.fromEntries(Object.entries(userColumns).map(([name, column]) => [name, column.meta])),
445
+ tombstone: tombstoneColumnMeta
446
+ };
447
+ this.baseName = options?.baseName;
448
+ this.description = options?.description;
449
+ }
450
+ describe(description) {
451
+ return new TableBuilder(this.userColumns, {
452
+ baseName: this.baseName,
453
+ description
454
+ });
455
+ }
456
+ /** Type-only: the row shape returned by queries. Do not access at runtime. */
457
+ get $row() {
458
+ return null;
459
+ }
460
+ /**
461
+ * Validate a CRDT event payload against the declared columns.
462
+ * `item-created` payloads must contain every column without a default (nullable columns may be omitted);
463
+ * `item-updated` payloads are partial and must not touch `id` or set `tombstone` to a deleted value.
464
+ */
465
+ validatePayload(payload, options) {
466
+ const event = options?.event ?? "item-created";
467
+ const errors = [];
468
+ for (const key of Object.keys(payload)) if (!(key in this.columns)) errors.push(`Unknown column "${key}"`);
469
+ if (event === "item-updated") {
470
+ if ("id" in payload) errors.push(`Column "id" is immutable and cannot appear in an update payload`);
471
+ if (payload.tombstone === true || payload.tombstone === 1) errors.push(`Column "tombstone" cannot be set to true/1 in an update payload; use an item-deleted event`);
472
+ }
473
+ if (event === "item-created") for (const [name, meta] of Object.entries(this.columns)) {
474
+ if (name in payload || name === "tombstone" || meta.nullable || meta.hasDefault) continue;
475
+ errors.push(`Missing required column "${name}"`);
476
+ }
477
+ for (const [name, value] of Object.entries(payload)) {
478
+ const meta = this.columns[name];
479
+ if (!meta || event === "item-updated" && name === "id") continue;
480
+ const error = validateValue(name, value, meta);
481
+ if (error) errors.push(error);
482
+ }
483
+ return errors.length === 0 ? { success: true } : {
484
+ success: false,
485
+ errors
486
+ };
487
+ }
622
488
  };
623
489
  function describeValue(value) {
624
- if (value === null) return "null";
625
- if (Array.isArray(value)) return "array";
626
- return typeof value;
490
+ if (value === null) return "null";
491
+ if (Array.isArray(value)) return "array";
492
+ return typeof value;
627
493
  }
628
494
  function validateValue(name, value, meta) {
629
- if (value === null) {
630
- return meta.nullable ? null : `Column "${name}" is not nullable, got null`;
631
- }
632
- switch (meta.kind) {
633
- case "text":
634
- return typeof value === "string" ? null : `Column "${name}" expects text, got ${describeValue(value)}`;
635
- case "integer":
636
- return typeof value === "number" && Number.isInteger(value) ? null : `Column "${name}" expects an integer, got ${describeValue(value)}`;
637
- case "real":
638
- return typeof value === "number" && Number.isFinite(value) ? null : `Column "${name}" expects a number, got ${describeValue(value)}`;
639
- case "boolean":
640
- return typeof value === "boolean" || value === 0 || value === 1 ? null : `Column "${name}" expects a boolean (true/false or 0/1), got ${describeValue(value)}`;
641
- case "enum": {
642
- if (typeof value !== "string") {
643
- return `Column "${name}" expects one of ${formatEnumValues(meta)}, got ${describeValue(value)}`;
644
- }
645
- return meta.enumValues?.includes(value) ? null : `Column "${name}" expects one of ${formatEnumValues(meta)}, got "${value}"`;
646
- }
647
- }
495
+ if (value === null) return meta.nullable ? null : `Column "${name}" is not nullable, got null`;
496
+ switch (meta.kind) {
497
+ case "text": return typeof value === "string" ? null : `Column "${name}" expects text, got ${describeValue(value)}`;
498
+ case "integer": return typeof value === "number" && Number.isInteger(value) ? null : `Column "${name}" expects an integer, got ${describeValue(value)}`;
499
+ case "real": return typeof value === "number" && Number.isFinite(value) ? null : `Column "${name}" expects a number, got ${describeValue(value)}`;
500
+ case "boolean": return typeof value === "boolean" || value === 0 || value === 1 ? null : `Column "${name}" expects a boolean (true/false or 0/1), got ${describeValue(value)}`;
501
+ case "enum":
502
+ if (typeof value !== "string") return `Column "${name}" expects one of ${formatEnumValues(meta)}, got ${describeValue(value)}`;
503
+ return meta.enumValues?.includes(value) ? null : `Column "${name}" expects one of ${formatEnumValues(meta)}, got "${value}"`;
504
+ }
648
505
  }
649
506
  function formatEnumValues(meta) {
650
- return (meta.enumValues ?? []).map((value) => `"${value}"`).join(" | ");
507
+ return (meta.enumValues ?? []).map((value) => `"${value}"`).join(" | ");
651
508
  }
652
509
  function column(kind, sqlType) {
653
- return new ColumnBuilder({ kind, sqlType, nullable: false, hasDefault: false });
510
+ return new ColumnBuilder({
511
+ kind,
512
+ sqlType,
513
+ nullable: false,
514
+ hasDefault: false
515
+ });
654
516
  }
655
- var t = {
656
- text: () => column("text", "text"),
657
- integer: () => column("integer", "integer"),
658
- real: () => column("real", "real"),
659
- /** Stored as INTEGER 0/1; always bind booleans as 0/1 when writing raw SQL. */
660
- boolean: () => column("boolean", "integer"),
661
- /**
662
- * Union of string literals stored as TEXT, e.g. `t.enum(["movie", "tv"])`. Unlike `$type`, the
663
- * allowed values are kept as runtime metadata, so payloads are validated against them.
664
- * For an open-ended set, use `t.text().$type<"idle" | (string & {})>()` instead.
665
- */
666
- enum: (values) => new ColumnBuilder({
667
- kind: "enum",
668
- sqlType: "text",
669
- nullable: false,
670
- hasDefault: false,
671
- enumValues: values
672
- }),
673
- table: (columns, options) => new TableBuilder(columns, options)
517
+ const t = {
518
+ text: () => column("text", "text"),
519
+ integer: () => column("integer", "integer"),
520
+ real: () => column("real", "real"),
521
+ /** Stored as INTEGER 0/1; always bind booleans as 0/1 when writing raw SQL. */
522
+ boolean: () => column("boolean", "integer"),
523
+ /**
524
+ * Union of string literals stored as TEXT, e.g. `t.enum(["movie", "tv"])`. Unlike `$type`, the
525
+ * allowed values are kept as runtime metadata, so payloads are validated against them.
526
+ * For an open-ended set, use `t.text().$type<"idle" | (string & {})>()` instead.
527
+ */
528
+ enum: (values) => new ColumnBuilder({
529
+ kind: "enum",
530
+ sqlType: "text",
531
+ nullable: false,
532
+ hasDefault: false,
533
+ enumValues: values
534
+ }),
535
+ table: (columns, options) => new TableBuilder(columns, options)
674
536
  };
675
-
676
- // src/sqlite-crdt/crdt-storage-mutator.ts
537
+ //#endregion
538
+ //#region src/sqlite-crdt/crdt-storage-mutator.ts
677
539
  function createCrdtStorageMutator({ storage }) {
678
- const mapToStorageEvent = (event) => {
679
- switch (event.type) {
680
- case "item-created":
681
- return {
682
- type: "item-created",
683
- dataset: event.dataset,
684
- item_id: event.item_id,
685
- payload: JSON.stringify(event.payload)
686
- };
687
- case "item-updated":
688
- return {
689
- type: "item-updated",
690
- dataset: event.dataset,
691
- item_id: event.item_id,
692
- payload: JSON.stringify(event.payload)
693
- };
694
- case "item-deleted":
695
- return {
696
- type: "item-deleted",
697
- dataset: event.dataset,
698
- item_id: event.item_id,
699
- payload: "{}"
700
- };
701
- }
702
- };
703
- const enqueueEvents = (events) => {
704
- storage.enqueueOwnEvents(events.map(mapToStorageEvent));
705
- };
706
- const createEvent = (event) => {
707
- return event;
708
- };
709
- const enqueueEvent = (event) => {
710
- storage.enqueueOwnEvents([mapToStorageEvent(event)]);
711
- };
712
- return {
713
- enqueueEvents,
714
- createEvent,
715
- enqueueEvent
716
- };
717
- }
718
-
719
- // src/sqlite-crdt/make-crdt-table.ts
720
- function makeCrdtTable({
721
- db,
722
- baseTableName,
723
- crdtTableName
724
- }) {
725
- const tableSchema = db.dbSchema[baseTableName];
726
- if (!tableSchema) {
727
- throw new Error(`Table ${baseTableName} not found`);
728
- }
729
- const columns = new Map(tableSchema.columns.map((c) => [c.name, c]));
730
- const idColumn = columns.get("id");
731
- if (!idColumn) {
732
- throw new Error(
733
- `Table "${baseTableName}" is missing a required "id" column. CRDT tables must have an "id" column to identify items.`
734
- );
735
- }
736
- if (idColumn.dataType.toUpperCase() !== "TEXT") {
737
- throw new Error(
738
- `Table "${baseTableName}": "id" column must be of type TEXT, got "${idColumn.dataType}". CRDT item IDs are stored as strings.`
739
- );
740
- }
741
- const tombstoneColumn = columns.get("tombstone");
742
- if (!tombstoneColumn) {
743
- throw new Error(
744
- `Table "${baseTableName}" is missing a required "tombstone" column. CRDT tables must have a "tombstone" INTEGER column for soft deletes.`
745
- );
746
- }
747
- const tombstoneType = tombstoneColumn.dataType.toUpperCase();
748
- if (tombstoneType !== "INTEGER" && tombstoneType !== "BOOLEAN") {
749
- throw new Error(
750
- `Table "${baseTableName}": "tombstone" column must be of type INTEGER or BOOLEAN, got "${tombstoneColumn.dataType}". It is compared as 0/1 for soft deletes.`
751
- );
752
- }
753
- db.execute(
754
- `
755
- create view ${quoteId(crdtTableName)} as
756
- select * from ${quoteId(baseTableName)}
757
- where tombstone = 0;`,
758
- { loggerLevel: "system" }
759
- );
760
- const allColumnNames = tableSchema.columns.map((column2) => column2.name);
761
- const jsonPayload = (from) => `'{'||${allColumnNames.map((col) => `'"${col}":'||json_quote(${from}.${quoteId(col)})`).join("||','||")}||'}'`;
762
- db.execute(
763
- `
764
- create trigger ${quoteId(`${crdtTableName}_created`)}
765
- instead of insert on ${quoteId(crdtTableName)}
766
- for each row
767
- begin
768
- select handle_item_created('${baseTableName}', ${jsonPayload("new")});
769
- end;
770
- `,
771
- { loggerLevel: "system" }
772
- );
773
- db.execute(
774
- `
775
- create trigger ${quoteId(`${crdtTableName}_updated`)}
776
- instead of update on ${quoteId(crdtTableName)}
777
- for each row
778
- begin
779
- select handle_item_updated(
780
- '${baseTableName}',
781
- ${jsonPayload("old")},
782
- ${jsonPayload("new")}
783
- );
784
- end;
785
- `,
786
- { loggerLevel: "system" }
787
- );
788
- db.execute(
789
- `
790
- create trigger ${quoteId(`${crdtTableName}_deleted`)}
791
- instead of delete on ${quoteId(crdtTableName)}
792
- for each row
793
- when old.tombstone = 0
794
- begin
795
- select handle_item_deleted('${baseTableName}', old.id);
796
- end;
797
- `,
798
- { loggerLevel: "system" }
799
- );
800
- }
801
- function registerCrdtFunctions({
802
- reactiveDb,
803
- storage
804
- }) {
805
- let eventApplied = false;
806
- reactiveDb.db.createScalarFunction({
807
- name: "handle_item_created",
808
- deterministic: false,
809
- directOnly: false,
810
- innocuous: false,
811
- callback: (dataset, payloadRaw) => {
812
- const payload = JSON.parse(payloadRaw);
813
- storage.internal.applyOwnEventFromTransaction(reactiveDb.db, {
814
- type: "item-created",
815
- dataset,
816
- item_id: payload.id,
817
- payload: payloadRaw
818
- });
819
- eventApplied = true;
820
- return void 0;
821
- }
822
- });
823
- reactiveDb.db.createScalarFunction({
824
- name: "handle_item_updated",
825
- deterministic: false,
826
- directOnly: false,
827
- innocuous: false,
828
- callback: (dataset, oldPayloadRaw, newPayloadRaw) => {
829
- if (oldPayloadRaw === newPayloadRaw) {
830
- return void 0;
831
- }
832
- const tableSchema = reactiveDb.db.dbSchema[dataset];
833
- if (!tableSchema?.columns) {
834
- throw new Error(`Schema not found for dataset: ${dataset}`);
835
- }
836
- const oldPayload = JSON.parse(oldPayloadRaw);
837
- const newPayload = JSON.parse(newPayloadRaw);
838
- let hasDiff = false;
839
- const updatePayload = {};
840
- for (const column2 of tableSchema.columns) {
841
- const oldValue = oldPayload[column2.name];
842
- const newValue = newPayload[column2.name];
843
- if (oldValue === newValue) {
844
- continue;
845
- }
846
- if (column2.name === "id") {
847
- throw new Error(
848
- `Cannot update the "id" column of an item. It is used to identify the item and must be immutable.`
849
- );
850
- }
851
- hasDiff = true;
852
- updatePayload[column2.name] = newValue;
853
- }
854
- if (!hasDiff) {
855
- return;
856
- }
857
- storage.internal.applyOwnEventFromTransaction(reactiveDb.db, {
858
- type: "item-updated",
859
- dataset,
860
- item_id: oldPayload.id,
861
- payload: JSON.stringify(updatePayload)
862
- });
863
- eventApplied = true;
864
- return void 0;
865
- }
866
- });
867
- reactiveDb.db.createScalarFunction({
868
- name: "handle_item_deleted",
869
- deterministic: false,
870
- directOnly: false,
871
- innocuous: false,
872
- callback: (dataset, itemId) => {
873
- storage.internal.applyOwnEventFromTransaction(reactiveDb.db, {
874
- type: "item-deleted",
875
- dataset,
876
- item_id: itemId,
877
- payload: "{}"
878
- });
879
- eventApplied = true;
880
- return void 0;
881
- }
882
- });
883
- reactiveDb.addEventListener("transaction-committed", () => {
884
- if (eventApplied) {
885
- eventApplied = false;
886
- void storage.internal.processEnqueuedEvents();
887
- }
888
- });
889
- reactiveDb.addEventListener("transaction-rolled-back", () => {
890
- eventApplied = false;
891
- });
540
+ const mapToStorageEvent = (event) => {
541
+ switch (event.type) {
542
+ case "item-created": return {
543
+ type: "item-created",
544
+ dataset: event.dataset,
545
+ item_id: event.item_id,
546
+ payload: JSON.stringify(event.payload)
547
+ };
548
+ case "item-updated": return {
549
+ type: "item-updated",
550
+ dataset: event.dataset,
551
+ item_id: event.item_id,
552
+ payload: JSON.stringify(event.payload)
553
+ };
554
+ case "item-deleted": return {
555
+ type: "item-deleted",
556
+ dataset: event.dataset,
557
+ item_id: event.item_id,
558
+ payload: "{}"
559
+ };
560
+ }
561
+ };
562
+ const enqueueEvents = (events) => {
563
+ storage.enqueueOwnEvents(events.map(mapToStorageEvent));
564
+ };
565
+ const createEvent = (event) => {
566
+ return event;
567
+ };
568
+ const enqueueEvent = (event) => {
569
+ storage.enqueueOwnEvents([mapToStorageEvent(event)]);
570
+ };
571
+ const enqueueSnapshot = (snapshot) => {
572
+ storage.applyOwnSnapshot({
573
+ dataset: snapshot.dataset,
574
+ item_id: snapshot.id,
575
+ patch: { ...snapshot.patch }
576
+ });
577
+ };
578
+ return {
579
+ enqueueEvents,
580
+ createEvent,
581
+ enqueueEvent,
582
+ enqueueSnapshot
583
+ };
892
584
  }
893
-
894
- // src/db-id.ts
895
- var dbIdRegex = /^[a-zA-Z][a-zA-Z\-0-9]{2,63}$/;
585
+ //#endregion
586
+ //#region src/db-id.ts
587
+ const dbIdRegex = /^[a-zA-Z][a-zA-Z\-0-9]{2,63}$/;
896
588
  function validateDbId(dbId) {
897
- if (!dbIdRegex.test(dbId)) {
898
- throw new Error("Invalid dbId. Must be between 3 and 64 characters long and start with a letter.");
899
- }
589
+ if (!dbIdRegex.test(dbId)) throw new Error("Invalid dbId. Must be between 3 and 64 characters long and start with a letter.");
900
590
  }
901
-
902
- // src/devtools-registry.ts
903
- var devtoolsRegistrySymbol = Symbol.for("@sqlite-sync/devtools");
591
+ //#endregion
592
+ //#region src/devtools-registry.ts
593
+ const devtoolsRegistrySymbol = Symbol.for("@sqlite-sync/devtools");
904
594
  function createSQLiteSyncDevtoolsRegistry() {
905
- const instances = /* @__PURE__ */ new Map();
906
- const listeners = /* @__PURE__ */ new Set();
907
- let snapshot = {
908
- instances: []
909
- };
910
- const notify = () => {
911
- for (const listener of listeners) {
912
- listener();
913
- }
914
- };
915
- const updateSnapshot = () => {
916
- snapshot = {
917
- instances: Array.from(instances.values())
918
- };
919
- notify();
920
- };
921
- return {
922
- version: 1,
923
- instances,
924
- subscribe(listener) {
925
- listeners.add(listener);
926
- return () => {
927
- listeners.delete(listener);
928
- };
929
- },
930
- getSnapshot() {
931
- return snapshot;
932
- },
933
- register(instance) {
934
- instances.set(instance.instanceId, instance);
935
- updateSnapshot();
936
- let isUnregistered = false;
937
- return () => {
938
- if (isUnregistered) return;
939
- isUnregistered = true;
940
- if (!instances.delete(instance.instanceId)) return;
941
- updateSnapshot();
942
- };
943
- }
944
- };
595
+ const instances = /* @__PURE__ */ new Map();
596
+ const listeners = /* @__PURE__ */ new Set();
597
+ let snapshot = { instances: [] };
598
+ const notify = () => {
599
+ for (const listener of listeners) listener();
600
+ };
601
+ const updateSnapshot = () => {
602
+ snapshot = { instances: Array.from(instances.values()) };
603
+ notify();
604
+ };
605
+ return {
606
+ version: 1,
607
+ instances,
608
+ subscribe(listener) {
609
+ listeners.add(listener);
610
+ return () => {
611
+ listeners.delete(listener);
612
+ };
613
+ },
614
+ getSnapshot() {
615
+ return snapshot;
616
+ },
617
+ register(instance) {
618
+ instances.set(instance.instanceId, instance);
619
+ updateSnapshot();
620
+ let isUnregistered = false;
621
+ return () => {
622
+ if (isUnregistered) return;
623
+ isUnregistered = true;
624
+ if (!instances.delete(instance.instanceId)) return;
625
+ updateSnapshot();
626
+ };
627
+ }
628
+ };
945
629
  }
946
630
  function getOrCreateSQLiteSyncDevtoolsRegistry() {
947
- const registryGlobal = globalThis;
948
- registryGlobal[devtoolsRegistrySymbol] ??= createSQLiteSyncDevtoolsRegistry();
949
- return registryGlobal[devtoolsRegistrySymbol];
631
+ const registryGlobal = globalThis;
632
+ registryGlobal[devtoolsRegistrySymbol] ??= createSQLiteSyncDevtoolsRegistry();
633
+ return registryGlobal[devtoolsRegistrySymbol];
950
634
  }
951
-
952
- // src/memory-db/memory-db.ts
953
- async function createMemoryDb({
954
- nodeId,
955
- migrator,
956
- reactiveDb: _reactiveDb,
957
- hlcCounter,
958
- crdtTables,
959
- initializeSchema = true,
960
- initialSyncId,
961
- eventHlcAccumulator,
962
- syncDbSchema
963
- }) {
964
- await xxhash.ensureLoaded();
965
- const reactiveDb = _reactiveDb;
966
- const db = reactiveDb.db;
967
- if (initializeSchema) {
968
- applyMemoryDbSchema(db);
969
- for (const table of crdtTables) {
970
- makeCrdtTable({
971
- db,
972
- baseTableName: table.baseTableName,
973
- crdtTableName: table.crdtTableName
974
- });
975
- }
976
- }
977
- const crdtStorage = createCrdtStorage({
978
- nodeId,
979
- initialLocalSyncId: initialSyncId ?? getCurrentSyncId(db),
980
- hlc: hlcCounter,
981
- migrator,
982
- db,
983
- dbConfig: memoryDbConfig,
984
- eventHlcAccumulator,
985
- schema: syncDbSchema
986
- });
987
- registerCrdtFunctions({
988
- reactiveDb,
989
- storage: crdtStorage
990
- });
991
- return {
992
- crdtStorage
993
- };
635
+ //#endregion
636
+ //#region src/memory-db/memory-db.ts
637
+ async function createMemoryDb({ nodeId, migrator, reactiveDb: _reactiveDb, hlcCounter, crdtTables, initializeSchema = true, initialSyncId, eventHlcAccumulator, syncDbSchema }) {
638
+ await xxhash.ensureLoaded();
639
+ const reactiveDb = _reactiveDb;
640
+ const db = reactiveDb.db;
641
+ if (initializeSchema) {
642
+ applyMemoryDbSchema(db);
643
+ for (const table of crdtTables) makeCrdtTable({
644
+ db,
645
+ baseTableName: table.baseTableName,
646
+ crdtTableName: table.crdtTableName
647
+ });
648
+ }
649
+ const crdtStorage = createCrdtStorage({
650
+ nodeId,
651
+ initialLocalSyncId: initialSyncId ?? getCurrentSyncId(db),
652
+ hlc: hlcCounter,
653
+ migrator,
654
+ db,
655
+ dbConfig: memoryDbConfig,
656
+ eventHlcAccumulator,
657
+ schema: syncDbSchema
658
+ });
659
+ registerCrdtIntentDrainer({
660
+ reactiveDb,
661
+ storage: crdtStorage
662
+ });
663
+ return { crdtStorage };
994
664
  }
995
665
  function getCurrentSyncId(db) {
996
- return db.execute("SELECT coalesce(max(sync_id), 0) AS syncId FROM persisted_crdt_events", {
997
- loggerLevel: "system"
998
- }).rows[0]?.syncId ?? 0;
666
+ return db.execute("SELECT coalesce(max(sync_id), 0) AS syncId FROM persisted_crdt_events", { loggerLevel: "system" }).rows[0]?.syncId ?? 0;
999
667
  }
1000
-
1001
- // src/worker-db/db-worker-client.ts
1002
- var createWorkerDbClient = async ({
1003
- broadcastChannels,
1004
- worker,
1005
- config
1006
- }) => {
1007
- const eventTarget = createTypedEventTarget();
1008
- const workerRequestsMap = /* @__PURE__ */ new Map();
1009
- let isDisposed = false;
1010
- const queryWorker = (method, args) => {
1011
- if (isDisposed) {
1012
- return Promise.reject(new Error("Worker client disposed"));
1013
- }
1014
- const requestId = crypto.randomUUID();
1015
- const promise = createDeferredPromise({
1016
- timeout: 3e4,
1017
- onTimeout: () => workerRequestsMap.delete(requestId)
1018
- });
1019
- workerRequestsMap.set(requestId, promise);
1020
- const request = {
1021
- type: "request",
1022
- requestId,
1023
- method,
1024
- args
1025
- };
1026
- broadcastChannels.requests.postMessage(request);
1027
- return promise.promise;
1028
- };
1029
- const handleWorkerResponse = (message) => {
1030
- const promise = workerRequestsMap.get(message.requestId);
1031
- if (!promise) {
1032
- return;
1033
- }
1034
- promise.resolve(message.data);
1035
- workerRequestsMap.delete(message.requestId);
1036
- };
1037
- const handleWorkerError = (message) => {
1038
- const promise = workerRequestsMap.get(message.requestId);
1039
- if (!promise) {
1040
- return;
1041
- }
1042
- promise.reject(new Error(message.error));
1043
- workerRequestsMap.delete(message.requestId);
1044
- };
1045
- broadcastChannels.responses.onmessage = (event) => {
1046
- const message = event.data;
1047
- if (isWorkerResponseMessage(message)) {
1048
- handleWorkerResponse(message);
1049
- } else if (isWorkerErrorResponseMessage(message)) {
1050
- handleWorkerError(message);
1051
- } else if (isWorkerNotificationMessage(message)) {
1052
- eventTarget.dispatchEvent(message.notificationType, message);
1053
- }
1054
- };
1055
- const rpc = {
1056
- execute: (query) => queryWorker("execute", [query]),
1057
- getSnapshot: () => queryWorker("getSnapshot", []),
1058
- importData: (data, opts) => queryWorker("importData", [data, opts]),
1059
- pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
1060
- pullEvents: (params) => queryWorker("pullEvents", [params]),
1061
- postState: () => queryWorker("postState", []),
1062
- goOnline: () => queryWorker("goOnline", []),
1063
- goOffline: () => queryWorker("goOffline", []),
1064
- requestReload: (options) => queryWorker("requestReload", [options])
1065
- };
1066
- const statePromise = awaitWorkerState(eventTarget);
1067
- postWorkerConfig(worker, config);
1068
- rpc.postState().catch(noop);
1069
- let workerState = await statePromise;
1070
- eventTarget.addEventListener("state-changed", (event) => {
1071
- workerState = event.payload.state;
1072
- });
1073
- const dispose = () => {
1074
- isDisposed = true;
1075
- broadcastChannels.responses.onmessage = null;
1076
- for (const [id, deferred] of workerRequestsMap) {
1077
- deferred.reject(new Error("Worker client disposed"));
1078
- workerRequestsMap.delete(id);
1079
- }
1080
- };
1081
- return {
1082
- ...rpc,
1083
- subscribe: eventTarget.addEventListener,
1084
- getState: () => workerState,
1085
- dispose
1086
- };
668
+ //#endregion
669
+ //#region src/worker-db/db-worker-client.ts
670
+ const createWorkerDbClient = async ({ broadcastChannels, worker, config }) => {
671
+ const eventTarget = createTypedEventTarget();
672
+ const workerRequestsMap = /* @__PURE__ */ new Map();
673
+ let isDisposed = false;
674
+ const queryWorker = (method, args) => {
675
+ if (isDisposed) return Promise.reject(/* @__PURE__ */ new Error("Worker client disposed"));
676
+ const requestId = crypto.randomUUID();
677
+ const promise = createDeferredPromise({
678
+ timeout: 3e4,
679
+ onTimeout: () => workerRequestsMap.delete(requestId)
680
+ });
681
+ workerRequestsMap.set(requestId, promise);
682
+ const request = {
683
+ type: "request",
684
+ requestId,
685
+ method,
686
+ args
687
+ };
688
+ broadcastChannels.requests.postMessage(request);
689
+ return promise.promise;
690
+ };
691
+ const handleWorkerResponse = (message) => {
692
+ const promise = workerRequestsMap.get(message.requestId);
693
+ if (!promise) return;
694
+ promise.resolve(message.data);
695
+ workerRequestsMap.delete(message.requestId);
696
+ };
697
+ const handleWorkerError = (message) => {
698
+ const promise = workerRequestsMap.get(message.requestId);
699
+ if (!promise) return;
700
+ promise.reject(new Error(message.error));
701
+ workerRequestsMap.delete(message.requestId);
702
+ };
703
+ broadcastChannels.responses.onmessage = (event) => {
704
+ const message = event.data;
705
+ if (isWorkerResponseMessage(message)) handleWorkerResponse(message);
706
+ else if (isWorkerErrorResponseMessage(message)) handleWorkerError(message);
707
+ else if (isWorkerNotificationMessage(message)) eventTarget.dispatchEvent(message.notificationType, message);
708
+ };
709
+ const rpc = {
710
+ execute: (query) => queryWorker("execute", [query]),
711
+ getSnapshot: () => queryWorker("getSnapshot", []),
712
+ importData: (data, opts) => queryWorker("importData", [data, opts]),
713
+ pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
714
+ pullEvents: (params) => queryWorker("pullEvents", [params]),
715
+ postState: () => queryWorker("postState", []),
716
+ goOnline: () => queryWorker("goOnline", []),
717
+ goOffline: () => queryWorker("goOffline", []),
718
+ requestReload: (options) => queryWorker("requestReload", [options])
719
+ };
720
+ const statePromise = awaitWorkerState(eventTarget);
721
+ postWorkerConfig(worker, config);
722
+ rpc.postState().catch(noop);
723
+ let workerState = await statePromise;
724
+ eventTarget.addEventListener("state-changed", (event) => {
725
+ workerState = event.payload.state;
726
+ });
727
+ const dispose = () => {
728
+ isDisposed = true;
729
+ broadcastChannels.responses.onmessage = null;
730
+ for (const [id, deferred] of workerRequestsMap) {
731
+ deferred.reject(/* @__PURE__ */ new Error("Worker client disposed"));
732
+ workerRequestsMap.delete(id);
733
+ }
734
+ };
735
+ return {
736
+ ...rpc,
737
+ subscribe: eventTarget.addEventListener,
738
+ getState: () => workerState,
739
+ dispose
740
+ };
1087
741
  };
1088
742
  function awaitWorkerState(eventTarget) {
1089
- const promise = createDeferredPromise({ timeout: 15e3 });
1090
- const subscription = eventTarget.addEventListener("state-changed", (event) => {
1091
- promise.resolve(event.payload.state);
1092
- subscription.unsubscribe();
1093
- });
1094
- return promise.promise;
743
+ const promise = createDeferredPromise({ timeout: 15e3 });
744
+ const subscription = eventTarget.addEventListener("state-changed", (event) => {
745
+ promise.resolve(event.payload.state);
746
+ subscription.unsubscribe();
747
+ });
748
+ return promise.promise;
1095
749
  }
1096
750
  function postWorkerConfig(worker, config) {
1097
- const configMessage = {
1098
- type: "init",
1099
- config
1100
- };
1101
- worker.postMessage(configMessage);
751
+ const configMessage = {
752
+ type: "init",
753
+ config
754
+ };
755
+ worker.postMessage(configMessage);
1102
756
  }
1103
-
1104
- // src/sync-db.ts
1105
- var defaultLogger = (type, message, level = "info") => {
1106
- const logMessage = `[${type}] ${message}`;
1107
- switch (level) {
1108
- case "info":
1109
- console.log(logMessage);
1110
- break;
1111
- case "warning":
1112
- console.warn(logMessage);
1113
- break;
1114
- case "error":
1115
- console.error(logMessage);
1116
- break;
1117
- case "trace":
1118
- console.trace(logMessage);
1119
- break;
1120
- }
757
+ //#endregion
758
+ //#region src/sync-db.ts
759
+ function createSqlExecutor(tx, unsafe) {
760
+ return {
761
+ execute: (query) => tx.execute(query, unsafe ? { skipAfterMutatingStatement: true } : void 0),
762
+ executeKysely: (factory) => tx.executeKysely(factory, unsafe ? { skipAfterMutatingStatement: true } : void 0)
763
+ };
764
+ }
765
+ /** @internal Exported from this module for focused facade tests, but not from the package entry point. */
766
+ function createSyncedDbDatabase(reactiveDb) {
767
+ const safeExecutor = createSqlExecutor(reactiveDb.db, false);
768
+ const unsafeExecutor = createSqlExecutor(reactiveDb.db, true);
769
+ return {
770
+ ...safeExecutor,
771
+ unsafe: unsafeExecutor,
772
+ executeTransaction: (callback) => reactiveDb.db.executeTransaction((tx) => {
773
+ const safeTransactionExecutor = createSqlExecutor(tx, false);
774
+ const unsafeTransactionExecutor = createSqlExecutor(tx, true);
775
+ return callback({
776
+ ...safeTransactionExecutor,
777
+ unsafe: unsafeTransactionExecutor,
778
+ sql: tx.sql.bind(tx)
779
+ });
780
+ }),
781
+ createLiveQuery: reactiveDb.createLiveQuery.bind(reactiveDb),
782
+ getSharedLiveQuery: reactiveDb.getSharedLiveQuery.bind(reactiveDb)
783
+ };
784
+ }
785
+ const defaultLogger = (type, message, level = "info") => {
786
+ const logMessage = `[${type}] ${message}`;
787
+ switch (level) {
788
+ case "info":
789
+ console.log(logMessage);
790
+ break;
791
+ case "warning":
792
+ console.warn(logMessage);
793
+ break;
794
+ case "error":
795
+ console.error(logMessage);
796
+ break;
797
+ case "trace": console.trace(logMessage);
798
+ }
1121
799
  };
1122
800
  async function createSyncedDb(options) {
1123
- validateDbId(options.dbId);
1124
- const perf = startPerformanceLogger(defaultLogger);
1125
- const instanceId = generateId();
1126
- const tabId = generateId();
1127
- const broadcastChannels = createBroadcastChannels(options.dbId);
1128
- const clientLockAcquired = createDeferredPromise();
1129
- const clientLockRelease = createDeferredPromise();
1130
- navigator.locks.request(`${syncDbClientLockName}-${options.dbId}`, { mode: "shared" }, () => {
1131
- clientLockAcquired.resolve();
1132
- return clientLockRelease.promise;
1133
- });
1134
- await clientLockAcquired.promise;
1135
- const workerClient = await createWorkerDbClient({
1136
- worker: options.worker,
1137
- config: {
1138
- clientId: generateId(),
1139
- dbId: options.dbId,
1140
- props: options.workerProps
1141
- },
1142
- broadcastChannels
1143
- });
1144
- const hlcCounter = new HLCCounter(tabId, () => Date.now());
1145
- const workerClientSnapshot = await workerClient.getSnapshot();
1146
- const reactiveDb = await createSQLiteReactiveDb({
1147
- snapshot: workerClientSnapshot.file,
1148
- logger: defaultLogger
1149
- });
1150
- const memoryDbMigrator = {
1151
- currentSchemaVersion: workerClientSnapshot.schemaVersion,
1152
- latestSchemaVersion: workerClientSnapshot.schemaVersion,
1153
- migrateDbToLatest: () => {
1154
- throw new Error("Memory DB migrations are not implemented");
1155
- },
1156
- migrateEvent: (event, targetVersion) => {
1157
- if (event.schema_version === targetVersion) {
1158
- return event;
1159
- }
1160
- throw new Error("Memory DB migrations are not implemented");
1161
- },
1162
- migrateEvents: (events) => events
1163
- };
1164
- const { crdtStorage } = await createMemoryDb({
1165
- nodeId: tabId,
1166
- migrator: memoryDbMigrator,
1167
- reactiveDb,
1168
- hlcCounter,
1169
- crdtTables: options.syncDbSchema.tablesConfig,
1170
- syncDbSchema: options.syncDbSchema
1171
- });
1172
- const pullSyncId = createStoredValue({
1173
- initialValue: workerClientSnapshot.syncId
1174
- });
1175
- const pushSyncId = createStoredValue({
1176
- initialValue: 0
1177
- });
1178
- const tabRemoteSource = createCrdtSyncRemoteSource({
1179
- bufferSize: 500,
1180
- pullSyncId,
1181
- pushSyncId,
1182
- storage: crdtStorage,
1183
- nodeId: tabId,
1184
- migrator: memoryDbMigrator,
1185
- remoteFactory: ({ onEventsAvailable }) => {
1186
- const subscription = workerClient.subscribe("new-event-chunk-applied", (event) => {
1187
- onEventsAvailable({ newSyncId: event.payload.newSyncId, remoteEventHlcSum: event.payload.eventHlcSum });
1188
- });
1189
- return {
1190
- pullEvents: (request) => workerClient.pullEvents(request),
1191
- pushEvents: (request) => workerClient.pushTabEvents(request),
1192
- disconnect: () => {
1193
- subscription.unsubscribe();
1194
- }
1195
- };
1196
- }
1197
- });
1198
- tabRemoteSource.goOnline();
1199
- const reloadRequestedSubscription = workerClient.subscribe("reload-requested", () => {
1200
- globalThis.location?.reload();
1201
- });
1202
- perf.logEnd("createSyncedDb", "initialized", "info");
1203
- const exportData = createExportData({
1204
- reactiveDb,
1205
- tablesConfig: options.syncDbSchema.tablesConfig,
1206
- schemaVersion: workerClientSnapshot.schemaVersion
1207
- });
1208
- const importData = (data, opts) => workerClient.importData(data, opts);
1209
- let isDisposed = false;
1210
- let unregisterDevtools;
1211
- const dispose = async () => {
1212
- if (isDisposed) return;
1213
- isDisposed = true;
1214
- unregisterDevtools?.();
1215
- reloadRequestedSubscription.unsubscribe();
1216
- clientLockRelease.resolve();
1217
- await tabRemoteSource.dispose();
1218
- broadcastChannels.requests.close();
1219
- broadcastChannels.responses.close();
1220
- workerClient.dispose();
1221
- reactiveDb.dispose();
1222
- };
1223
- const syncedDb = {
1224
- db: {
1225
- execute: reactiveDb.db.execute.bind(reactiveDb.db),
1226
- executeKysely: reactiveDb.db.executeKysely.bind(reactiveDb.db),
1227
- executeTransaction: reactiveDb.db.executeTransaction.bind(reactiveDb.db),
1228
- createLiveQuery: reactiveDb.createLiveQuery.bind(reactiveDb),
1229
- getSharedLiveQuery: reactiveDb.getSharedLiveQuery.bind(reactiveDb)
1230
- },
1231
- state: {
1232
- getState: workerClient.getState.bind(workerClient),
1233
- subscribe: (onChange) => {
1234
- const { unsubscribe } = workerClient.subscribe("state-changed", onChange);
1235
- return unsubscribe;
1236
- },
1237
- goOnline: workerClient.goOnline.bind(workerClient),
1238
- goOffline: workerClient.goOffline.bind(workerClient)
1239
- },
1240
- /**
1241
- * Ask the elected worker to broadcast a page reload to all tabs for this dbId.
1242
- *
1243
- * With `clean: true` the worker durably records a reset request epoch before
1244
- * broadcasting, so the worker elected on the next startup initializes with
1245
- * `clearOnInit: true` and wipes the persisted DB. Destructive — use as a
1246
- * recovery path when the durable worker DB may be de-synced.
1247
- *
1248
- * Pending in-memory tab events are not preserved, and the returned promise
1249
- * may never settle in the caller — the page typically unloads first.
1250
- */
1251
- requestReload: async (options2) => {
1252
- await workerClient.requestReload(options2);
1253
- setTimeout(() => globalThis.location?.reload(), 250);
1254
- },
1255
- subscribe: workerClient.subscribe,
1256
- exportData,
1257
- importData,
1258
- dispose,
1259
- _internal: {
1260
- executeAsync: workerClient.execute.bind(workerClient),
1261
- getMemoryQueryTables: reactiveDb.getTablesUsed.bind(reactiveDb),
1262
- getSharedLiveQueriesSnapshot: reactiveDb.getSharedLiveQueriesSnapshot.bind(reactiveDb),
1263
- crdtTableNames: options.syncDbSchema.tablesConfig.map((table) => table.crdtTableName),
1264
- crdtTablesConfig: options.syncDbSchema.tablesConfig,
1265
- schemaVersion: workerClientSnapshot.schemaVersion,
1266
- migrationVersions: Object.keys(options.syncDbSchema.migrations).map(Number).sort((a, b) => a - b)
1267
- }
1268
- };
1269
- unregisterDevtools = getOrCreateSQLiteSyncDevtoolsRegistry().register({
1270
- instanceId,
1271
- dbId: options.dbId,
1272
- createdAt: Date.now(),
1273
- instance: syncedDb
1274
- });
1275
- return syncedDb;
801
+ validateDbId(options.dbId);
802
+ const perf = startPerformanceLogger(defaultLogger);
803
+ const instanceId = generateId();
804
+ const tabId = generateId();
805
+ const broadcastChannels = createBroadcastChannels(options.dbId);
806
+ const clientLockAcquired = createDeferredPromise();
807
+ const clientLockRelease = createDeferredPromise();
808
+ navigator.locks.request(`${syncDbClientLockName}-${options.dbId}`, { mode: "shared" }, () => {
809
+ clientLockAcquired.resolve();
810
+ return clientLockRelease.promise;
811
+ });
812
+ await clientLockAcquired.promise;
813
+ const workerClient = await createWorkerDbClient({
814
+ worker: options.worker,
815
+ config: {
816
+ clientId: generateId(),
817
+ dbId: options.dbId,
818
+ props: options.workerProps
819
+ },
820
+ broadcastChannels
821
+ });
822
+ const hlcCounter = new HLCCounter(tabId, () => Date.now());
823
+ const workerClientSnapshot = await workerClient.getSnapshot();
824
+ const reactiveDb = await createSQLiteReactiveDb({
825
+ snapshot: workerClientSnapshot.file,
826
+ logger: defaultLogger
827
+ });
828
+ const memoryDbMigrator = {
829
+ currentSchemaVersion: workerClientSnapshot.schemaVersion,
830
+ latestSchemaVersion: workerClientSnapshot.schemaVersion,
831
+ migrateDbToLatest: () => {
832
+ throw new Error("Memory DB migrations are not implemented");
833
+ },
834
+ migrateEvent: (event, targetVersion) => {
835
+ if (event.schema_version === targetVersion) return event;
836
+ throw new Error("Memory DB migrations are not implemented");
837
+ },
838
+ migrateEvents: (events) => events
839
+ };
840
+ const { crdtStorage } = await createMemoryDb({
841
+ nodeId: tabId,
842
+ migrator: memoryDbMigrator,
843
+ reactiveDb,
844
+ hlcCounter,
845
+ crdtTables: options.syncDbSchema.tablesConfig,
846
+ syncDbSchema: options.syncDbSchema
847
+ });
848
+ const pullSyncId = createStoredValue({ initialValue: workerClientSnapshot.syncId });
849
+ const pushSyncId = createStoredValue({ initialValue: 0 });
850
+ const tabRemoteSource = createCrdtSyncRemoteSource({
851
+ bufferSize: 500,
852
+ pullSyncId,
853
+ pushSyncId,
854
+ storage: crdtStorage,
855
+ nodeId: tabId,
856
+ migrator: memoryDbMigrator,
857
+ remoteFactory: ({ onEventsAvailable }) => {
858
+ const subscription = workerClient.subscribe("new-event-chunk-applied", (event) => {
859
+ onEventsAvailable({
860
+ newSyncId: event.payload.newSyncId,
861
+ remoteEventHlcSum: event.payload.eventHlcSum
862
+ });
863
+ });
864
+ return {
865
+ pullEvents: (request) => workerClient.pullEvents(request),
866
+ pushEvents: (request) => workerClient.pushTabEvents(request),
867
+ disconnect: () => {
868
+ subscription.unsubscribe();
869
+ }
870
+ };
871
+ }
872
+ });
873
+ tabRemoteSource.goOnline();
874
+ const reloadRequestedSubscription = workerClient.subscribe("reload-requested", () => {
875
+ globalThis.location?.reload();
876
+ });
877
+ perf.logEnd("createSyncedDb", "initialized", "info");
878
+ const exportData = createExportData({
879
+ reactiveDb,
880
+ tablesConfig: options.syncDbSchema.tablesConfig,
881
+ schemaVersion: workerClientSnapshot.schemaVersion
882
+ });
883
+ const importData = (data, opts) => workerClient.importData(data, opts);
884
+ let isDisposed = false;
885
+ let unregisterDevtools;
886
+ const dispose = async () => {
887
+ if (isDisposed) return;
888
+ isDisposed = true;
889
+ unregisterDevtools?.();
890
+ reloadRequestedSubscription.unsubscribe();
891
+ clientLockRelease.resolve();
892
+ await tabRemoteSource.dispose();
893
+ broadcastChannels.requests.close();
894
+ broadcastChannels.responses.close();
895
+ workerClient.dispose();
896
+ reactiveDb.dispose();
897
+ };
898
+ const syncedDb = {
899
+ db: createSyncedDbDatabase(reactiveDb),
900
+ state: {
901
+ getState: workerClient.getState.bind(workerClient),
902
+ subscribe: (onChange) => {
903
+ const { unsubscribe } = workerClient.subscribe("state-changed", onChange);
904
+ return unsubscribe;
905
+ },
906
+ goOnline: workerClient.goOnline.bind(workerClient),
907
+ goOffline: workerClient.goOffline.bind(workerClient)
908
+ },
909
+ /**
910
+ * Ask the elected worker to broadcast a page reload to all tabs for this dbId.
911
+ *
912
+ * With `clean: true` the worker durably records a reset request epoch before
913
+ * broadcasting, so the worker elected on the next startup initializes with
914
+ * `clearOnInit: true` and wipes the persisted DB. Destructive — use as a
915
+ * recovery path when the durable worker DB may be de-synced.
916
+ *
917
+ * Pending in-memory tab events are not preserved, and the returned promise
918
+ * may never settle in the caller — the page typically unloads first.
919
+ */
920
+ requestReload: async (options) => {
921
+ await workerClient.requestReload(options);
922
+ setTimeout(() => globalThis.location?.reload(), 250);
923
+ },
924
+ subscribe: workerClient.subscribe,
925
+ exportData,
926
+ importData,
927
+ dispose,
928
+ _internal: {
929
+ executeAsync: workerClient.execute.bind(workerClient),
930
+ getMemoryQueryTables: reactiveDb.getTablesUsed.bind(reactiveDb),
931
+ getSharedLiveQueriesSnapshot: reactiveDb.getSharedLiveQueriesSnapshot.bind(reactiveDb),
932
+ crdtTableNames: options.syncDbSchema.tablesConfig.map((table) => table.crdtTableName),
933
+ crdtTablesConfig: options.syncDbSchema.tablesConfig,
934
+ schemaVersion: workerClientSnapshot.schemaVersion,
935
+ migrationVersions: Object.keys(options.syncDbSchema.migrations).map(Number).sort((a, b) => a - b)
936
+ }
937
+ };
938
+ unregisterDevtools = getOrCreateSQLiteSyncDevtoolsRegistry().register({
939
+ instanceId,
940
+ dbId: options.dbId,
941
+ createdAt: Date.now(),
942
+ instance: syncedDb
943
+ });
944
+ return syncedDb;
1276
945
  }
1277
- export {
1278
- CRDT_EVENT_NO_OP_PAYLOAD,
1279
- ColumnBuilder,
1280
- CrdtEventValidationError,
1281
- HLCCounter,
1282
- RESET_REQUEST_TTL_MS,
1283
- SQLiteDbWrapper,
1284
- SQLiteReactiveDb,
1285
- TableBuilder,
1286
- TypedBroadcastChannel,
1287
- TypedEvent,
1288
- applyMemoryDbSchema,
1289
- applyWorkerDbSchema,
1290
- baseSystemMigrations,
1291
- compareHLC,
1292
- createCrdtApplyFunction,
1293
- createCrdtStorage,
1294
- createCrdtStorageMutator,
1295
- createCrdtSyncProducer,
1296
- createCrdtSyncRemoteSource,
1297
- createDeferredPromise,
1298
- createExportData,
1299
- createImportData,
1300
- createKvStoreTableQuery,
1301
- createMigrations,
1302
- createMigrator,
1303
- createSQLiteCrdtApplyFunction,
1304
- createSQLiteKvStore,
1305
- createStoredValue,
1306
- createSyncedDb,
1307
- createSystemDbConfig,
1308
- createTypedEventTarget,
1309
- defineSyncSchema,
1310
- deserializeHLC,
1311
- dummyKysely,
1312
- formatSchemaVerificationIssues,
1313
- generateId,
1314
- introspectDb,
1315
- isNoOpCrdtEventPayload,
1316
- jsonSafeParse,
1317
- makeCrdtTable,
1318
- parametersAreEqual,
1319
- quoteId,
1320
- runSystemMigrations,
1321
- serializeHLC,
1322
- startPerformanceLogger,
1323
- t,
1324
- tryCatch,
1325
- tryCatchAsync,
1326
- validateNewCrdtEvent,
1327
- verifySyncSchema,
1328
- xxhash
1329
- };
946
+ //#endregion
947
+ export { CRDT_EVENT_NO_OP_PAYLOAD, ColumnBuilder, CrdtEventValidationError, HLCCounter, RESET_REQUEST_TTL_MS, SQLiteDbWrapper, SQLiteReactiveDb, TableBuilder, TypedBroadcastChannel, TypedEvent, applyMemoryDbSchema, applyWorkerDbSchema, compareHLC, createCrdtApplyFunction, createCrdtStorage, createCrdtStorageMutator, createCrdtSyncProducer, createCrdtSyncRemoteSource, createDeferredPromise, createExportData, createImportData, createKvStoreTableQuery, createMigrations, createMigrator, createSQLiteCrdtApplyFunction, createSQLiteKvStore, createStoredValue, createSyncedDb, createTypedEventTarget, defineSyncSchema, deserializeHLC, dummyKysely, formatSchemaVerificationIssues, generateId, introspectDb, isNoOpCrdtEventPayload, jsonSafeParse, makeCrdtTable, parametersAreEqual, quoteId, serializeHLC, startPerformanceLogger, t, tryCatch, tryCatchAsync, validateNewCrdtEvent, verifySyncSchema, xxhash };
948
+
1330
949
  //# sourceMappingURL=index.js.map