@sqlite-sync/core 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1330 +1,1013 @@
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-CvpTHmRY.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/admit-client-events.ts
337
+ function buildWriteOriginByName(schema) {
338
+ const writeOriginByName = /* @__PURE__ */ new Map();
339
+ for (const { crdtTableName, baseTableName } of schema.tablesConfig) {
340
+ const writeOrigin = schema.tables[crdtTableName]?.writeOrigin ?? "any";
341
+ writeOriginByName.set(crdtTableName, writeOrigin);
342
+ writeOriginByName.set(baseTableName, writeOrigin);
343
+ }
344
+ return writeOriginByName;
345
+ }
346
+ /**
347
+ * Splits a client push into events the hub should persist and events for tables declared
348
+ * `.writes("server")`. Unknown datasets stay admitted. Resolves either the crdt or the base
349
+ * table name.
350
+ */
351
+ function admitClientEvents(opts) {
352
+ const writeOriginByName = opts.syncDbSchema.writeOriginByName ?? buildWriteOriginByName(opts.syncDbSchema);
353
+ const admitted = [];
354
+ const skipped = [];
355
+ for (const event of opts.events) if (writeOriginByName.get(event.dataset) === "server") skipped.push(event);
356
+ else admitted.push(event);
357
+ return {
358
+ admitted,
359
+ skipped
360
+ };
361
+ }
362
+ //#endregion
363
+ //#region src/schema/define-sync-schema.ts
364
+ const RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
365
+ "change_history",
366
+ "crdt_events",
367
+ "worker.crdt_events",
368
+ "persisted_crdt_events",
369
+ "crdt_update_log",
370
+ "crdt_change_intents",
371
+ "worker.kv"
475
372
  ]);
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
- };
373
+ /**
374
+ * Define a sync database schema from `t.table()` builders.
375
+ *
376
+ * Record keys are the crdt (view) table names; base table names default to the key
377
+ * prefixed with "_", overridable per table via `t.table(cols, { baseName })`.
378
+ */
379
+ function defineSyncSchema({ tables, migrations }) {
380
+ const tablesConfig = Object.entries(tables).map(([crdtTableName, table]) => ({
381
+ crdtTableName,
382
+ baseTableName: table.baseName ?? `_${crdtTableName}`
383
+ }));
384
+ const seenNames = /* @__PURE__ */ new Set();
385
+ for (const { crdtTableName, baseTableName } of tablesConfig) for (const name of [crdtTableName, baseTableName]) {
386
+ const normalizedName = name.toLowerCase();
387
+ 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`);
388
+ if (seenNames.has(normalizedName)) throw new Error(`Duplicate table name "${name}" in sync schema`);
389
+ seenNames.add(normalizedName);
390
+ }
391
+ const writeOriginByName = buildWriteOriginByName({
392
+ tables,
393
+ tablesConfig
394
+ });
395
+ return {
396
+ tables,
397
+ get tablesConfig() {
398
+ return tablesConfig;
399
+ },
400
+ get writeOriginByName() {
401
+ return writeOriginByName;
402
+ },
403
+ get migrations() {
404
+ return migrations;
405
+ },
406
+ get "~clientSchema"() {
407
+ throw new Error("~clientSchema is type-only and cannot be accessed at runtime");
408
+ },
409
+ get "~serverSchema"() {
410
+ throw new Error("~serverSchema is type-only and cannot be accessed at runtime");
411
+ },
412
+ get "~mutationsSchema"() {
413
+ throw new Error("~mutationsSchema is type-only and cannot be accessed at runtime");
414
+ }
415
+ };
514
416
  }
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
- }
417
+ //#endregion
418
+ //#region src/schema/table-builder.ts
419
+ var ColumnBuilder = class ColumnBuilder {
420
+ meta;
421
+ constructor(meta) {
422
+ this.meta = meta;
423
+ }
424
+ /** Allow SQL NULL; widens the value type with `null`. */
425
+ nullable() {
426
+ return new ColumnBuilder({
427
+ ...this.meta,
428
+ nullable: true
429
+ });
430
+ }
431
+ /** Declare a column default. The value is used both in DDL and when migrating older events. */
432
+ default(value) {
433
+ return new ColumnBuilder({
434
+ ...this.meta,
435
+ hasDefault: true,
436
+ defaultValue: value
437
+ });
438
+ }
439
+ /** Narrow the TypeScript value type without changing storage, e.g. `t.text().$type<"movie" | "tv">()`. */
440
+ $type() {
441
+ return this;
442
+ }
443
+ /** Human/AI-readable description, surfaced in generated schema docs. */
444
+ describe(description) {
445
+ return new ColumnBuilder({
446
+ ...this.meta,
447
+ description
448
+ });
449
+ }
537
450
  };
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"
451
+ const RESERVED_COLUMNS = ["id", "tombstone"];
452
+ const idColumnMeta = {
453
+ kind: "text",
454
+ sqlType: "text",
455
+ nullable: false,
456
+ hasDefault: false,
457
+ description: "Unique immutable item id"
545
458
  };
546
- var tombstoneColumnMeta = {
547
- kind: "boolean",
548
- sqlType: "integer",
549
- nullable: false,
550
- hasDefault: true,
551
- defaultValue: false,
552
- description: "Soft-delete marker"
459
+ const tombstoneColumnMeta = {
460
+ kind: "boolean",
461
+ sqlType: "integer",
462
+ nullable: false,
463
+ hasDefault: true,
464
+ defaultValue: false,
465
+ description: "Soft-delete marker"
553
466
  };
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
- }
467
+ var TableBuilder = class TableBuilder {
468
+ userColumns;
469
+ /** All columns in DDL order, including the auto-injected `id` and `tombstone`. */
470
+ columns;
471
+ baseName;
472
+ description;
473
+ #aiAccess = "read-write";
474
+ #writeOrigin = "any";
475
+ /** AI access declared via `.ai()`; "read-write" unless narrowed. */
476
+ get aiAccess() {
477
+ return this.#aiAccess;
478
+ }
479
+ /** Who may author events for this table; "any" unless narrowed via `.writes()`. */
480
+ get writeOrigin() {
481
+ return this.#writeOrigin;
482
+ }
483
+ constructor(userColumns, options) {
484
+ this.userColumns = userColumns;
485
+ 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`);
486
+ this.columns = {
487
+ id: idColumnMeta,
488
+ ...Object.fromEntries(Object.entries(userColumns).map(([name, column]) => [name, column.meta])),
489
+ tombstone: tombstoneColumnMeta
490
+ };
491
+ this.baseName = options?.baseName;
492
+ this.description = options?.description;
493
+ }
494
+ /** Rebuilds the table with `overrides` applied, carrying over state kept outside the options. */
495
+ withOptions(overrides) {
496
+ const next = new TableBuilder(this.userColumns, {
497
+ baseName: this.baseName,
498
+ description: this.description,
499
+ ...overrides
500
+ });
501
+ next.#aiAccess = this.#aiAccess;
502
+ next.#writeOrigin = this.#writeOrigin;
503
+ return next;
504
+ }
505
+ describe(description) {
506
+ return this.withOptions({ description });
507
+ }
508
+ /** Restrict what an AI agent may do with this table; see {@link AiAccess}. */
509
+ ai(access) {
510
+ const next = this.withOptions({});
511
+ next.#aiAccess = access;
512
+ return next;
513
+ }
514
+ /** Restrict which replicas may author events for this table; see {@link WriteOrigin}. */
515
+ writes(origin) {
516
+ const next = this.withOptions({});
517
+ next.#writeOrigin = origin;
518
+ return next;
519
+ }
520
+ /** Type-only: the row shape returned by queries. Do not access at runtime. */
521
+ get $row() {
522
+ return null;
523
+ }
524
+ /**
525
+ * Validate a CRDT event payload against the declared columns.
526
+ * `item-created` payloads must contain every column without a default (nullable columns may be omitted);
527
+ * `item-updated` payloads are partial and must not touch `id` or set `tombstone` to a deleted value.
528
+ */
529
+ validatePayload(payload, options) {
530
+ const event = options?.event ?? "item-created";
531
+ const errors = [];
532
+ for (const key of Object.keys(payload)) if (!(key in this.columns)) errors.push(`Unknown column "${key}"`);
533
+ if (event === "item-updated") {
534
+ if ("id" in payload) errors.push(`Column "id" is immutable and cannot appear in an update payload`);
535
+ 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`);
536
+ }
537
+ if (event === "item-created") for (const [name, meta] of Object.entries(this.columns)) {
538
+ if (name in payload || name === "tombstone" || meta.nullable || meta.hasDefault) continue;
539
+ errors.push(`Missing required column "${name}"`);
540
+ }
541
+ for (const [name, value] of Object.entries(payload)) {
542
+ const meta = this.columns[name];
543
+ if (!meta || event === "item-updated" && name === "id") continue;
544
+ const error = validateValue(name, value, meta);
545
+ if (error) errors.push(error);
546
+ }
547
+ return errors.length === 0 ? { success: true } : {
548
+ success: false,
549
+ errors
550
+ };
551
+ }
622
552
  };
623
553
  function describeValue(value) {
624
- if (value === null) return "null";
625
- if (Array.isArray(value)) return "array";
626
- return typeof value;
554
+ if (value === null) return "null";
555
+ if (Array.isArray(value)) return "array";
556
+ return typeof value;
627
557
  }
628
558
  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
- }
559
+ if (value === null) return meta.nullable ? null : `Column "${name}" is not nullable, got null`;
560
+ switch (meta.kind) {
561
+ case "text": return typeof value === "string" ? null : `Column "${name}" expects text, got ${describeValue(value)}`;
562
+ case "integer": return typeof value === "number" && Number.isInteger(value) ? null : `Column "${name}" expects an integer, got ${describeValue(value)}`;
563
+ case "real": return typeof value === "number" && Number.isFinite(value) ? null : `Column "${name}" expects a number, got ${describeValue(value)}`;
564
+ case "boolean": return typeof value === "boolean" || value === 0 || value === 1 ? null : `Column "${name}" expects a boolean (true/false or 0/1), got ${describeValue(value)}`;
565
+ case "enum":
566
+ if (typeof value !== "string") return `Column "${name}" expects one of ${formatEnumValues(meta)}, got ${describeValue(value)}`;
567
+ return meta.enumValues?.includes(value) ? null : `Column "${name}" expects one of ${formatEnumValues(meta)}, got "${value}"`;
568
+ }
648
569
  }
649
570
  function formatEnumValues(meta) {
650
- return (meta.enumValues ?? []).map((value) => `"${value}"`).join(" | ");
571
+ return (meta.enumValues ?? []).map((value) => `"${value}"`).join(" | ");
651
572
  }
652
573
  function column(kind, sqlType) {
653
- return new ColumnBuilder({ kind, sqlType, nullable: false, hasDefault: false });
574
+ return new ColumnBuilder({
575
+ kind,
576
+ sqlType,
577
+ nullable: false,
578
+ hasDefault: false
579
+ });
654
580
  }
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)
581
+ const t = {
582
+ text: () => column("text", "text"),
583
+ integer: () => column("integer", "integer"),
584
+ real: () => column("real", "real"),
585
+ /** Stored as INTEGER 0/1; always bind booleans as 0/1 when writing raw SQL. */
586
+ boolean: () => column("boolean", "integer"),
587
+ /**
588
+ * Union of string literals stored as TEXT, e.g. `t.enum(["movie", "tv"])`. Unlike `$type`, the
589
+ * allowed values are kept as runtime metadata, so payloads are validated against them.
590
+ * For an open-ended set, use `t.text().$type<"idle" | (string & {})>()` instead.
591
+ */
592
+ enum: (values) => new ColumnBuilder({
593
+ kind: "enum",
594
+ sqlType: "text",
595
+ nullable: false,
596
+ hasDefault: false,
597
+ enumValues: values
598
+ }),
599
+ table: (columns, options) => new TableBuilder(columns, options)
674
600
  };
675
-
676
- // src/sqlite-crdt/crdt-storage-mutator.ts
601
+ //#endregion
602
+ //#region src/sqlite-crdt/crdt-storage-mutator.ts
677
603
  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
- );
604
+ const mapToStorageEvent = (event) => {
605
+ switch (event.type) {
606
+ case "item-created": return {
607
+ type: "item-created",
608
+ dataset: event.dataset,
609
+ item_id: event.item_id,
610
+ payload: JSON.stringify(event.payload)
611
+ };
612
+ case "item-updated": return {
613
+ type: "item-updated",
614
+ dataset: event.dataset,
615
+ item_id: event.item_id,
616
+ payload: JSON.stringify(event.payload)
617
+ };
618
+ case "item-deleted": return {
619
+ type: "item-deleted",
620
+ dataset: event.dataset,
621
+ item_id: event.item_id,
622
+ payload: "{}"
623
+ };
624
+ }
625
+ };
626
+ const enqueueEvents = (events) => {
627
+ storage.enqueueOwnEvents(events.map(mapToStorageEvent));
628
+ };
629
+ const createEvent = (event) => {
630
+ return event;
631
+ };
632
+ const enqueueEvent = (event) => {
633
+ storage.enqueueOwnEvents([mapToStorageEvent(event)]);
634
+ };
635
+ const enqueueSnapshot = (snapshot) => {
636
+ storage.applyOwnSnapshot({
637
+ dataset: snapshot.dataset,
638
+ item_id: snapshot.id,
639
+ patch: { ...snapshot.patch }
640
+ });
641
+ };
642
+ return {
643
+ enqueueEvents,
644
+ createEvent,
645
+ enqueueEvent,
646
+ enqueueSnapshot
647
+ };
800
648
  }
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
- });
892
- }
893
-
894
- // src/db-id.ts
895
- var dbIdRegex = /^[a-zA-Z][a-zA-Z\-0-9]{2,63}$/;
649
+ //#endregion
650
+ //#region src/db-id.ts
651
+ const dbIdRegex = /^[a-zA-Z][a-zA-Z\-0-9]{2,63}$/;
896
652
  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
- }
653
+ if (!dbIdRegex.test(dbId)) throw new Error("Invalid dbId. Must be between 3 and 64 characters long and start with a letter.");
900
654
  }
901
-
902
- // src/devtools-registry.ts
903
- var devtoolsRegistrySymbol = Symbol.for("@sqlite-sync/devtools");
655
+ //#endregion
656
+ //#region src/devtools-registry.ts
657
+ const devtoolsRegistrySymbol = Symbol.for("@sqlite-sync/devtools");
904
658
  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
- };
659
+ const instances = /* @__PURE__ */ new Map();
660
+ const listeners = /* @__PURE__ */ new Set();
661
+ let snapshot = { instances: [] };
662
+ const notify = () => {
663
+ for (const listener of listeners) listener();
664
+ };
665
+ const updateSnapshot = () => {
666
+ snapshot = { instances: Array.from(instances.values()) };
667
+ notify();
668
+ };
669
+ return {
670
+ version: 1,
671
+ instances,
672
+ subscribe(listener) {
673
+ listeners.add(listener);
674
+ return () => {
675
+ listeners.delete(listener);
676
+ };
677
+ },
678
+ getSnapshot() {
679
+ return snapshot;
680
+ },
681
+ register(instance) {
682
+ instances.set(instance.instanceId, instance);
683
+ updateSnapshot();
684
+ let isUnregistered = false;
685
+ return () => {
686
+ if (isUnregistered) return;
687
+ isUnregistered = true;
688
+ if (!instances.delete(instance.instanceId)) return;
689
+ updateSnapshot();
690
+ };
691
+ }
692
+ };
945
693
  }
946
694
  function getOrCreateSQLiteSyncDevtoolsRegistry() {
947
- const registryGlobal = globalThis;
948
- registryGlobal[devtoolsRegistrySymbol] ??= createSQLiteSyncDevtoolsRegistry();
949
- return registryGlobal[devtoolsRegistrySymbol];
695
+ const registryGlobal = globalThis;
696
+ registryGlobal[devtoolsRegistrySymbol] ??= createSQLiteSyncDevtoolsRegistry();
697
+ return registryGlobal[devtoolsRegistrySymbol];
950
698
  }
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
- };
699
+ //#endregion
700
+ //#region src/memory-db/memory-db.ts
701
+ async function createMemoryDb({ nodeId, migrator, reactiveDb: _reactiveDb, hlcCounter, crdtTables, initializeSchema = true, initialSyncId, eventHlcAccumulator, syncDbSchema }) {
702
+ await xxhash.ensureLoaded();
703
+ const reactiveDb = _reactiveDb;
704
+ const db = reactiveDb.db;
705
+ if (initializeSchema) {
706
+ applyMemoryDbSchema(db);
707
+ for (const table of crdtTables) makeCrdtTable({
708
+ db,
709
+ baseTableName: table.baseTableName,
710
+ crdtTableName: table.crdtTableName
711
+ });
712
+ }
713
+ const crdtStorage = createCrdtStorage({
714
+ nodeId,
715
+ initialLocalSyncId: initialSyncId ?? getCurrentSyncId(db),
716
+ hlc: hlcCounter,
717
+ migrator,
718
+ db,
719
+ dbConfig: memoryDbConfig,
720
+ eventHlcAccumulator,
721
+ schema: syncDbSchema
722
+ });
723
+ registerCrdtIntentDrainer({
724
+ reactiveDb,
725
+ storage: crdtStorage
726
+ });
727
+ return { crdtStorage };
994
728
  }
995
729
  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;
730
+ return db.execute("SELECT coalesce(max(sync_id), 0) AS syncId FROM persisted_crdt_events", { loggerLevel: "system" }).rows[0]?.syncId ?? 0;
999
731
  }
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
- };
732
+ //#endregion
733
+ //#region src/worker-db/db-worker-client.ts
734
+ const createWorkerDbClient = async ({ broadcastChannels, worker, config }) => {
735
+ const eventTarget = createTypedEventTarget();
736
+ const workerRequestsMap = /* @__PURE__ */ new Map();
737
+ let isDisposed = false;
738
+ const queryWorker = (method, args) => {
739
+ if (isDisposed) return Promise.reject(/* @__PURE__ */ new Error("Worker client disposed"));
740
+ const requestId = crypto.randomUUID();
741
+ const promise = createDeferredPromise({
742
+ timeout: 3e4,
743
+ onTimeout: () => workerRequestsMap.delete(requestId)
744
+ });
745
+ workerRequestsMap.set(requestId, promise);
746
+ const request = {
747
+ type: "request",
748
+ requestId,
749
+ method,
750
+ args
751
+ };
752
+ broadcastChannels.requests.postMessage(request);
753
+ return promise.promise;
754
+ };
755
+ const handleWorkerResponse = (message) => {
756
+ const promise = workerRequestsMap.get(message.requestId);
757
+ if (!promise) return;
758
+ promise.resolve(message.data);
759
+ workerRequestsMap.delete(message.requestId);
760
+ };
761
+ const handleWorkerError = (message) => {
762
+ const promise = workerRequestsMap.get(message.requestId);
763
+ if (!promise) return;
764
+ promise.reject(new Error(message.error));
765
+ workerRequestsMap.delete(message.requestId);
766
+ };
767
+ broadcastChannels.responses.onmessage = (event) => {
768
+ const message = event.data;
769
+ if (isWorkerResponseMessage(message)) handleWorkerResponse(message);
770
+ else if (isWorkerErrorResponseMessage(message)) handleWorkerError(message);
771
+ else if (isWorkerNotificationMessage(message)) eventTarget.dispatchEvent(message.notificationType, message);
772
+ };
773
+ const rpc = {
774
+ execute: (query) => queryWorker("execute", [query]),
775
+ getSnapshot: () => queryWorker("getSnapshot", []),
776
+ importData: (data, opts) => queryWorker("importData", [data, opts]),
777
+ pushTabEvents: (request) => queryWorker("pushTabEvents", [request]),
778
+ pullEvents: (params) => queryWorker("pullEvents", [params]),
779
+ postState: () => queryWorker("postState", []),
780
+ goOnline: () => queryWorker("goOnline", []),
781
+ goOffline: () => queryWorker("goOffline", []),
782
+ requestReload: (options) => queryWorker("requestReload", [options])
783
+ };
784
+ const statePromise = awaitWorkerState(eventTarget);
785
+ postWorkerConfig(worker, config);
786
+ rpc.postState().catch(noop);
787
+ let workerState = await statePromise;
788
+ eventTarget.addEventListener("state-changed", (event) => {
789
+ workerState = event.payload.state;
790
+ });
791
+ const dispose = () => {
792
+ isDisposed = true;
793
+ broadcastChannels.responses.onmessage = null;
794
+ for (const [id, deferred] of workerRequestsMap) {
795
+ deferred.reject(/* @__PURE__ */ new Error("Worker client disposed"));
796
+ workerRequestsMap.delete(id);
797
+ }
798
+ };
799
+ return {
800
+ ...rpc,
801
+ subscribe: eventTarget.addEventListener,
802
+ getState: () => workerState,
803
+ dispose
804
+ };
1087
805
  };
1088
806
  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;
807
+ const promise = createDeferredPromise({ timeout: 15e3 });
808
+ const subscription = eventTarget.addEventListener("state-changed", (event) => {
809
+ promise.resolve(event.payload.state);
810
+ subscription.unsubscribe();
811
+ });
812
+ return promise.promise;
1095
813
  }
1096
814
  function postWorkerConfig(worker, config) {
1097
- const configMessage = {
1098
- type: "init",
1099
- config
1100
- };
1101
- worker.postMessage(configMessage);
815
+ const configMessage = {
816
+ type: "init",
817
+ config
818
+ };
819
+ worker.postMessage(configMessage);
1102
820
  }
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
- }
821
+ //#endregion
822
+ //#region src/sync-db.ts
823
+ function createSqlExecutor(tx, unsafe) {
824
+ return {
825
+ execute: (query) => tx.execute(query, unsafe ? { skipAfterMutatingStatement: true } : void 0),
826
+ executeKysely: (factory) => tx.executeKysely(factory, unsafe ? { skipAfterMutatingStatement: true } : void 0)
827
+ };
828
+ }
829
+ /** @internal Exported from this module for focused facade tests, but not from the package entry point. */
830
+ function createSyncedDbDatabase(reactiveDb) {
831
+ const safeExecutor = createSqlExecutor(reactiveDb.db, false);
832
+ const unsafeExecutor = createSqlExecutor(reactiveDb.db, true);
833
+ return {
834
+ ...safeExecutor,
835
+ unsafe: unsafeExecutor,
836
+ executeTransaction: (callback) => reactiveDb.db.executeTransaction((tx) => {
837
+ const safeTransactionExecutor = createSqlExecutor(tx, false);
838
+ const unsafeTransactionExecutor = createSqlExecutor(tx, true);
839
+ return callback({
840
+ ...safeTransactionExecutor,
841
+ unsafe: unsafeTransactionExecutor,
842
+ sql: tx.sql.bind(tx)
843
+ });
844
+ }),
845
+ createLiveQuery: reactiveDb.createLiveQuery.bind(reactiveDb),
846
+ getSharedLiveQuery: reactiveDb.getSharedLiveQuery.bind(reactiveDb)
847
+ };
848
+ }
849
+ const defaultLogger = (type, message, level = "info") => {
850
+ const logMessage = `[${type}] ${message}`;
851
+ switch (level) {
852
+ case "info":
853
+ console.log(logMessage);
854
+ break;
855
+ case "warning":
856
+ console.warn(logMessage);
857
+ break;
858
+ case "error":
859
+ console.error(logMessage);
860
+ break;
861
+ case "trace": console.trace(logMessage);
862
+ }
1121
863
  };
1122
864
  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;
865
+ validateDbId(options.dbId);
866
+ const perf = startPerformanceLogger(defaultLogger);
867
+ const instanceId = generateId();
868
+ const tabId = generateId();
869
+ const broadcastChannels = createBroadcastChannels(options.dbId);
870
+ const clientLockAcquired = createDeferredPromise();
871
+ const clientLockRelease = createDeferredPromise();
872
+ navigator.locks.request(`${syncDbClientLockName}-${options.dbId}`, { mode: "shared" }, () => {
873
+ clientLockAcquired.resolve();
874
+ return clientLockRelease.promise;
875
+ });
876
+ await clientLockAcquired.promise;
877
+ const workerClient = await createWorkerDbClient({
878
+ worker: options.worker,
879
+ config: {
880
+ clientId: generateId(),
881
+ dbId: options.dbId,
882
+ props: options.workerProps
883
+ },
884
+ broadcastChannels
885
+ });
886
+ const hlcCounter = new HLCCounter(tabId, () => Date.now());
887
+ const workerClientSnapshot = await workerClient.getSnapshot();
888
+ const reactiveDb = await createSQLiteReactiveDb({
889
+ snapshot: workerClientSnapshot.file,
890
+ logger: defaultLogger
891
+ });
892
+ const memoryDbMigrator = {
893
+ currentSchemaVersion: workerClientSnapshot.schemaVersion,
894
+ latestSchemaVersion: workerClientSnapshot.schemaVersion,
895
+ migrateDbToLatest: () => {
896
+ throw new Error("Memory DB migrations are not implemented");
897
+ },
898
+ migrateEvent: (event, targetVersion) => {
899
+ if (event.schema_version === targetVersion) return event;
900
+ throw new Error("Memory DB migrations are not implemented");
901
+ },
902
+ migrateEvents: (events) => events
903
+ };
904
+ const { crdtStorage } = await createMemoryDb({
905
+ nodeId: tabId,
906
+ migrator: memoryDbMigrator,
907
+ reactiveDb,
908
+ hlcCounter,
909
+ crdtTables: options.syncDbSchema.tablesConfig,
910
+ syncDbSchema: options.syncDbSchema
911
+ });
912
+ const pullSyncId = createStoredValue({ initialValue: workerClientSnapshot.syncId });
913
+ const pushSyncId = createStoredValue({ initialValue: 0 });
914
+ const tabRemoteSource = createCrdtSyncRemoteSource({
915
+ bufferSize: 500,
916
+ pullSyncId,
917
+ pushSyncId,
918
+ storage: crdtStorage,
919
+ nodeId: tabId,
920
+ migrator: memoryDbMigrator,
921
+ remoteFactory: ({ onEventsAvailable }) => {
922
+ const subscription = workerClient.subscribe("new-event-chunk-applied", (event) => {
923
+ onEventsAvailable({
924
+ newSyncId: event.payload.newSyncId,
925
+ remoteEventHlcSum: event.payload.eventHlcSum
926
+ });
927
+ });
928
+ return {
929
+ pullEvents: (request) => workerClient.pullEvents(request),
930
+ pushEvents: (request) => workerClient.pushTabEvents(request),
931
+ disconnect: () => {
932
+ subscription.unsubscribe();
933
+ }
934
+ };
935
+ }
936
+ });
937
+ tabRemoteSource.goOnline();
938
+ const reloadRequestedSubscription = workerClient.subscribe("reload-requested", () => {
939
+ globalThis.location?.reload();
940
+ });
941
+ perf.logEnd("createSyncedDb", "initialized", "info");
942
+ const exportData = createExportData({
943
+ reactiveDb,
944
+ tablesConfig: options.syncDbSchema.tablesConfig,
945
+ schemaVersion: workerClientSnapshot.schemaVersion
946
+ });
947
+ const importData = (data, opts) => workerClient.importData(data, opts);
948
+ let isDisposed = false;
949
+ let unregisterDevtools;
950
+ const dispose = async () => {
951
+ if (isDisposed) return;
952
+ isDisposed = true;
953
+ unregisterDevtools?.();
954
+ reloadRequestedSubscription.unsubscribe();
955
+ clientLockRelease.resolve();
956
+ await tabRemoteSource.dispose();
957
+ broadcastChannels.requests.close();
958
+ broadcastChannels.responses.close();
959
+ workerClient.dispose();
960
+ reactiveDb.dispose();
961
+ };
962
+ const syncedDb = {
963
+ db: createSyncedDbDatabase(reactiveDb),
964
+ state: {
965
+ getState: workerClient.getState.bind(workerClient),
966
+ subscribe: (onChange) => {
967
+ const { unsubscribe } = workerClient.subscribe("state-changed", onChange);
968
+ return unsubscribe;
969
+ },
970
+ goOnline: workerClient.goOnline.bind(workerClient),
971
+ goOffline: workerClient.goOffline.bind(workerClient)
972
+ },
973
+ /**
974
+ * Ask the elected worker to broadcast a page reload to all tabs for this dbId.
975
+ *
976
+ * With `clean: true` the worker durably records a reset request epoch before
977
+ * broadcasting, so the worker elected on the next startup initializes with
978
+ * `clearOnInit: true` and wipes the persisted DB. Destructive — use as a
979
+ * recovery path when the durable worker DB may be de-synced.
980
+ *
981
+ * Pending in-memory tab events are not preserved, and the returned promise
982
+ * may never settle in the caller — the page typically unloads first.
983
+ */
984
+ requestReload: async (options) => {
985
+ await workerClient.requestReload(options);
986
+ setTimeout(() => globalThis.location?.reload(), 250);
987
+ },
988
+ subscribe: workerClient.subscribe,
989
+ exportData,
990
+ importData,
991
+ dispose,
992
+ _internal: {
993
+ executeAsync: workerClient.execute.bind(workerClient),
994
+ getMemoryQueryTables: reactiveDb.getTablesUsed.bind(reactiveDb),
995
+ getSharedLiveQueriesSnapshot: reactiveDb.getSharedLiveQueriesSnapshot.bind(reactiveDb),
996
+ crdtTableNames: options.syncDbSchema.tablesConfig.map((table) => table.crdtTableName),
997
+ crdtTablesConfig: options.syncDbSchema.tablesConfig,
998
+ schemaVersion: workerClientSnapshot.schemaVersion,
999
+ migrationVersions: Object.keys(options.syncDbSchema.migrations).map(Number).sort((a, b) => a - b)
1000
+ }
1001
+ };
1002
+ unregisterDevtools = getOrCreateSQLiteSyncDevtoolsRegistry().register({
1003
+ instanceId,
1004
+ dbId: options.dbId,
1005
+ createdAt: Date.now(),
1006
+ instance: syncedDb
1007
+ });
1008
+ return syncedDb;
1276
1009
  }
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
- };
1010
+ //#endregion
1011
+ export { CRDT_EVENT_NO_OP_PAYLOAD, ColumnBuilder, CrdtEventValidationError, HLCCounter, RESET_REQUEST_TTL_MS, SQLiteDbWrapper, SQLiteReactiveDb, TableBuilder, TypedBroadcastChannel, TypedEvent, admitClientEvents, 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 };
1012
+
1330
1013
  //# sourceMappingURL=index.js.map