@sqlite-sync/core 0.8.1 → 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/worker.js CHANGED
@@ -1,139 +1,99 @@
1
- import {
2
- HLCCounter,
3
- SQLiteDbWrapper,
4
- applyWorkerDbSchema,
5
- createBroadcastChannels,
6
- createCrdtStorage,
7
- createCrdtSyncProducer,
8
- createCrdtSyncRemoteSource,
9
- createIdbResetStore,
10
- createImportData,
11
- createMigrator,
12
- createReloadRequestHandler,
13
- createResetStateStore,
14
- formatSchemaVerificationIssues,
15
- isWorkerInitMessage,
16
- isWorkerRequestMessage,
17
- syncDbClientLockName,
18
- syncDbWorkerLockName,
19
- verifySyncSchema,
20
- workerDbConfig,
21
- xxhash
22
- } from "./chunk-GMF45NFH.js";
23
- import {
24
- createDeferredPromise,
25
- jsonSafeParse
26
- } from "./chunk-NHT3ELMN.js";
27
-
28
- // src/web-socket/ws-remote-source.ts
29
- var createWsRemoteSource = ({ createWebSocket }) => {
30
- return async ({ onEventsAvailable }) => {
31
- const socket = createWebSocket();
32
- const openPromise = createDeferredPromise({
33
- timeout: 5e3,
34
- onTimeout: () => {
35
- socket.close();
36
- }
37
- });
38
- socket.addEventListener("open", () => {
39
- openPromise.resolve(void 0);
40
- });
41
- await openPromise.promise;
42
- const requestsMap = /* @__PURE__ */ new Map();
43
- const pushEvents = async (request) => {
44
- const requestId = crypto.randomUUID();
45
- const promise = createDeferredPromise({ timeout: 5e3 });
46
- requestsMap.set(requestId, promise);
47
- const wsRequest = {
48
- type: "push-events",
49
- requestId,
50
- nodeId: request.nodeId,
51
- events: request.events
52
- };
53
- socket.send(JSON.stringify(wsRequest));
54
- return promise.promise;
55
- };
56
- const pullEvents = async (request) => {
57
- const requestId = crypto.randomUUID();
58
- const promise = createDeferredPromise({ timeout: 2e3 });
59
- requestsMap.set(requestId, promise);
60
- const wsRequest = {
61
- type: "pull-events",
62
- requestId,
63
- afterSyncId: request.afterSyncId,
64
- excludeNodeId: request.excludeNodeId
65
- };
66
- socket.send(JSON.stringify(wsRequest));
67
- return promise.promise;
68
- };
69
- socket.onmessage = (event) => {
70
- const result = jsonSafeParse(event.data);
71
- if (!result.success || !("type" in result.data) || !result.data.type) {
72
- return;
73
- }
74
- const message = result.data;
75
- switch (message.type) {
76
- case "events-pull-response": {
77
- const promise = requestsMap.get(message.requestId);
78
- if (!promise) {
79
- return;
80
- }
81
- promise.resolve(message.data);
82
- requestsMap.delete(message.requestId);
83
- break;
84
- }
85
- case "events-push-response": {
86
- const promise = requestsMap.get(message.requestId);
87
- if (!promise) {
88
- return;
89
- }
90
- promise.resolve(message.data);
91
- requestsMap.delete(message.requestId);
92
- break;
93
- }
94
- case "events-applied":
95
- onEventsAvailable({ newSyncId: message.newSyncId, remoteEventHlcSum: message.eventHlcSum });
96
- break;
97
- default:
98
- message;
99
- return;
100
- }
101
- };
102
- return {
103
- pushEvents,
104
- pullEvents,
105
- disconnect: () => {
106
- socket.close();
107
- }
108
- };
109
- };
110
- };
111
-
112
- // src/worker-db/db-worker.ts
1
+ import { C as createMigrator, D as HLCCounter, N as createImportData, a as createBroadcastChannels, d as syncDbClientLockName, f as syncDbWorkerLockName, h as createCrdtStorage, i as createResetStateStore, j as xxhash, l as isWorkerRequestMessage, m as createCrdtSyncProducer, n as createIdbResetStore, p as createCrdtSyncRemoteSource, r as createReloadRequestHandler, s as isWorkerInitMessage, v as formatSchemaVerificationIssues, w as SQLiteDbWrapper, y as verifySyncSchema } from "./reset-state-D3nsfx_t.js";
2
+ import { r as createDeferredPromise, s as jsonSafeParse } from "./utils-Dr5JwcTP.js";
3
+ import { n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-CkglcfTS.js";
113
4
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
114
-
115
- // src/worker-db/event-log-gc.ts
116
- var WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS = 100;
5
+ //#region src/web-socket/ws-remote-source.ts
6
+ const createWsRemoteSource = ({ createWebSocket }) => {
7
+ return async ({ onEventsAvailable }) => {
8
+ const socket = createWebSocket();
9
+ const openPromise = createDeferredPromise({
10
+ timeout: 5e3,
11
+ onTimeout: () => {
12
+ socket.close();
13
+ }
14
+ });
15
+ socket.addEventListener("open", () => {
16
+ openPromise.resolve(void 0);
17
+ });
18
+ await openPromise.promise;
19
+ const requestsMap = /* @__PURE__ */ new Map();
20
+ const pushEvents = async (request) => {
21
+ const requestId = crypto.randomUUID();
22
+ const promise = createDeferredPromise({ timeout: 5e3 });
23
+ requestsMap.set(requestId, promise);
24
+ const wsRequest = {
25
+ type: "push-events",
26
+ requestId,
27
+ nodeId: request.nodeId,
28
+ events: request.events
29
+ };
30
+ socket.send(JSON.stringify(wsRequest));
31
+ return promise.promise;
32
+ };
33
+ const pullEvents = async (request) => {
34
+ const requestId = crypto.randomUUID();
35
+ const promise = createDeferredPromise({ timeout: 2e3 });
36
+ requestsMap.set(requestId, promise);
37
+ const wsRequest = {
38
+ type: "pull-events",
39
+ requestId,
40
+ afterSyncId: request.afterSyncId,
41
+ excludeNodeId: request.excludeNodeId
42
+ };
43
+ socket.send(JSON.stringify(wsRequest));
44
+ return promise.promise;
45
+ };
46
+ socket.onmessage = (event) => {
47
+ const result = jsonSafeParse(event.data);
48
+ if (!result.success || !("type" in result.data) || !result.data.type) return;
49
+ const message = result.data;
50
+ switch (message.type) {
51
+ case "events-pull-response": {
52
+ const promise = requestsMap.get(message.requestId);
53
+ if (!promise) return;
54
+ promise.resolve(message.data);
55
+ requestsMap.delete(message.requestId);
56
+ break;
57
+ }
58
+ case "events-push-response": {
59
+ const promise = requestsMap.get(message.requestId);
60
+ if (!promise) return;
61
+ promise.resolve(message.data);
62
+ requestsMap.delete(message.requestId);
63
+ break;
64
+ }
65
+ case "events-applied":
66
+ onEventsAvailable({
67
+ newSyncId: message.newSyncId,
68
+ remoteEventHlcSum: message.eventHlcSum
69
+ });
70
+ break;
71
+ default: return;
72
+ }
73
+ };
74
+ return {
75
+ pushEvents,
76
+ pullEvents,
77
+ disconnect: () => {
78
+ socket.close();
79
+ }
80
+ };
81
+ };
82
+ };
117
83
  function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, logger }) {
118
- const eventsTable = dbConfig.eventsTable.fullIdentifier;
119
- if (eventHlcAccumulator.current === "") {
120
- const [{ count }] = db.executePreparedRaw({
121
- key: "event-log-gc-count-applied-events",
122
- sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE "status" = 'applied'`,
123
- meta: { loggerLevel: "system" }
124
- });
125
- if (count > 0) {
126
- logger?.(
127
- "worker",
128
- "Skipping event log GC until the event HLC checksum has been computed from full history",
129
- "info"
130
- );
131
- }
132
- return { skipped: true };
133
- }
134
- const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
135
- key: "event-log-gc-delete-before-sync-id",
136
- sql: `
84
+ const eventsTable = dbConfig.eventsTable.fullIdentifier;
85
+ if (eventHlcAccumulator.current === "") {
86
+ const [{ count }] = db.executePreparedRaw({
87
+ key: "event-log-gc-count-applied-events",
88
+ sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE "status" = 'applied'`,
89
+ meta: { loggerLevel: "system" }
90
+ });
91
+ if (count > 0) logger?.("worker", "Skipping event log GC until the event HLC checksum has been computed from full history", "info");
92
+ return { skipped: true };
93
+ }
94
+ const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
95
+ key: "event-log-gc-delete-before-sync-id",
96
+ sql: `
137
97
  SELECT min("sync_id") AS deleteBeforeSyncId
138
98
  FROM (
139
99
  SELECT "sync_id"
@@ -143,369 +103,329 @@ function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, lo
143
103
  LIMIT ?
144
104
  )
145
105
  `,
146
- params: [WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS],
147
- meta: { loggerLevel: "system" }
148
- });
149
- if (deleteBeforeSyncId === null) {
150
- return { skipped: false };
151
- }
152
- db.executePreparedRaw({
153
- key: "event-log-gc-delete-events",
154
- sql: `
106
+ params: [100],
107
+ meta: { loggerLevel: "system" }
108
+ });
109
+ if (deleteBeforeSyncId === null) return { skipped: false };
110
+ db.executePreparedRaw({
111
+ key: "event-log-gc-delete-events",
112
+ sql: `
155
113
  DELETE FROM ${eventsTable}
156
114
  WHERE "sync_id" < ?
157
115
  AND "status" IN ('applied', 'deduped')
158
116
  AND ("origin" = 'remote' OR "sync_id" <= ?)
159
117
  `,
160
- params: [deleteBeforeSyncId, pushSyncId.current],
161
- meta: { loggerLevel: "system" }
162
- });
163
- logger?.("worker", "Event log GC completed", "info");
164
- return { skipped: false };
118
+ params: [deleteBeforeSyncId, pushSyncId.current],
119
+ meta: { loggerLevel: "system" }
120
+ });
121
+ logger?.("worker", "Event log GC completed", "info");
122
+ return { skipped: false };
165
123
  }
166
-
167
- // src/worker-db/storage-version.ts
168
- var LIB_STORAGE_VERSION = 1;
169
- var storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
124
+ const storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
170
125
  function formatStorageVersion(appStorageVersion) {
171
- return appStorageVersion === void 0 ? `lib-v${LIB_STORAGE_VERSION}` : `lib-v${LIB_STORAGE_VERSION}:app-${appStorageVersion}`;
126
+ return appStorageVersion === void 0 ? `lib-v2` : `lib-v2:app-${appStorageVersion}`;
172
127
  }
128
+ /**
129
+ * Worker-owned durable storage version. The current version combines the
130
+ * internal lib version with the dev-provided app version; when the stored
131
+ * version does not match (including a missing record), the elected worker
132
+ * initializes with `clearOnInit: true`. Wiping on a missing record is
133
+ * harmless for fresh installs and correctly resets databases persisted
134
+ * before versioning existed.
135
+ */
173
136
  function createStorageVersionStore({ store, dbId, appStorageVersion }) {
174
- const key = storageVersionKey(dbId);
175
- const currentVersion = formatStorageVersion(appStorageVersion);
176
- return {
177
- currentVersion,
178
- async isVersionMismatch() {
179
- const storedVersion = await store.get(key);
180
- return storedVersion !== currentVersion;
181
- },
182
- /**
183
- * Record the current version. Must be called only after the worker has
184
- * successfully initialized with the wiped DB, so a failed init can be
185
- * retried by a later elected worker.
186
- */
187
- async markCurrentVersionApplied() {
188
- await store.set(key, currentVersion);
189
- }
190
- };
137
+ const key = storageVersionKey(dbId);
138
+ const currentVersion = formatStorageVersion(appStorageVersion);
139
+ return {
140
+ currentVersion,
141
+ async isVersionMismatch() {
142
+ return await store.get(key) !== currentVersion;
143
+ },
144
+ /**
145
+ * Record the current version. Must be called only after the worker has
146
+ * successfully initialized with the wiped DB, so a failed init can be
147
+ * retried by a later elected worker.
148
+ */
149
+ async markCurrentVersionApplied() {
150
+ await store.set(key, currentVersion);
151
+ }
152
+ };
191
153
  }
192
-
193
- // src/worker-db/db-worker.ts
194
- var defaultLogger = (type, message, level = "info") => {
195
- const logMessage = `[${type}] ${message}`;
196
- switch (level) {
197
- case "info":
198
- console.log(logMessage);
199
- break;
200
- case "warning":
201
- console.warn(logMessage);
202
- break;
203
- case "error":
204
- console.error(logMessage);
205
- break;
206
- case "trace":
207
- console.trace(logMessage);
208
- break;
209
- }
154
+ //#endregion
155
+ //#region src/worker-db/db-worker.ts
156
+ const defaultLogger = (type, message, level = "info") => {
157
+ const logMessage = `[${type}] ${message}`;
158
+ switch (level) {
159
+ case "info":
160
+ console.log(logMessage);
161
+ break;
162
+ case "warning":
163
+ console.warn(logMessage);
164
+ break;
165
+ case "error":
166
+ console.error(logMessage);
167
+ break;
168
+ case "trace": console.trace(logMessage);
169
+ }
210
170
  };
211
171
  async function createDbWorker(config, opts) {
212
- const broadcastChannels = createBroadcastChannels(config.dbId);
213
- const logger = opts.logger ?? defaultLogger;
214
- if (opts.verifySchema) {
215
- const issues = await verifySyncSchema(opts.syncDbSchema);
216
- if (issues.length > 0) {
217
- throw new Error(formatSchemaVerificationIssues(issues));
218
- }
219
- logger("worker", "Schema verification passed: migrations match the declared schema", "info");
220
- }
221
- const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);
222
- const resetStore = opts.resetStore ?? createIdbResetStore();
223
- const resetState = createResetStateStore({
224
- store: resetStore,
225
- dbId: config.dbId
226
- });
227
- const storageVersion = createStorageVersionStore({
228
- store: resetStore,
229
- dbId: config.dbId,
230
- appStorageVersion: opts.storageVersion
231
- });
232
- const [pendingReset, isVersionMismatch] = await Promise.all([
233
- resetState.resolvePendingReset(),
234
- storageVersion.isVersionMismatch()
235
- ]);
236
- if (isVersionMismatch) {
237
- logger("worker", `Storage version mismatch \u2014 resetting local DB to ${storageVersion.currentVersion}`, "warning");
238
- }
239
- const pool = await sqlite3.installOpfsSAHPoolVfs({
240
- name: config.dbId,
241
- directory: `.${config.dbId}`,
242
- clearOnInit: !!pendingReset || isVersionMismatch,
243
- initialCapacity: 8
244
- });
245
- const db = new SQLiteDbWrapper({
246
- db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),
247
- logger,
248
- loggerPrefix: "worker",
249
- sqlite3
250
- });
251
- db.execute("PRAGMA locking_mode=exclusive", { loggerLevel: "system" });
252
- db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
253
- db.execute("PRAGMA synchronous=NORMAL", { loggerLevel: "system" });
254
- db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: "system" });
255
- db.execute("PRAGMA worker.locking_mode=exclusive", { loggerLevel: "system" });
256
- db.execute("PRAGMA worker.journal_mode=WAL", { loggerLevel: "system" });
257
- db.execute("PRAGMA worker.synchronous=NORMAL", { loggerLevel: "system" });
258
- const { kvStore } = applyWorkerDbSchema(db);
259
- const migrator = createMigrator({
260
- migrations: opts.syncDbSchema.migrations,
261
- schemaVersion: kvStore.createNumberStoredValue("schema-version", -1),
262
- updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier
263
- });
264
- migrator.migrateDbToLatest({
265
- startTransaction: (callback) => {
266
- db.executeTransaction(
267
- (tx) => callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) })
268
- );
269
- }
270
- });
271
- db.invalidateDbSchema();
272
- const pullSyncId = kvStore.createNumberStoredValue("pull-sync-id", -1);
273
- const pushSyncId = kvStore.createNumberStoredValue("push-sync-id", -1);
274
- const eventHlcAccumulator = kvStore.createStringStoredValue("crdt.consistency.event_hlc_sum.v2", "");
275
- if (pendingReset) {
276
- await resetState.markResetApplied(pendingReset.epoch);
277
- }
278
- if (isVersionMismatch) {
279
- await storageVersion.markCurrentVersionApplied();
280
- }
281
- if (opts.eventLogGc === true) {
282
- runWorkerEventLogGc({
283
- db,
284
- dbConfig: workerDbConfig,
285
- pushSyncId,
286
- eventHlcAccumulator,
287
- logger
288
- });
289
- }
290
- const crdtStorage = createCrdtStorage({
291
- nodeId: config.clientId,
292
- initialLocalSyncId: getMaxSyncId(db, "none"),
293
- migrator,
294
- hlc: new HLCCounter(config.clientId, () => Date.now()),
295
- db,
296
- dbConfig: workerDbConfig,
297
- eventHlcAccumulator,
298
- schema: opts.syncDbSchema
299
- });
300
- createCrdtSyncProducer({
301
- storage: crdtStorage,
302
- broadcastEvents: (chunk) => {
303
- broadcastChannels.responses.postMessage({
304
- notificationType: "new-event-chunk-applied",
305
- newSyncId: chunk.newSyncId,
306
- eventHlcSum: chunk.eventHlcSum
307
- });
308
- }
309
- });
310
- const remoteSource = createRemoteSource({
311
- crdtStorage,
312
- migrator,
313
- clientId: config.clientId,
314
- pullSyncId,
315
- pushSyncId,
316
- remoteFactory: opts.createRemoteSource
317
- });
318
- remoteSource.goOnline();
319
- const broadcastNotification = (notification) => {
320
- broadcastChannels.responses.postMessage(notification);
321
- };
322
- const postState = () => {
323
- broadcastNotification({
324
- notificationType: "state-changed",
325
- state: remoteSource.getState()
326
- });
327
- };
328
- const stateChangedSubscription = remoteSource.addEventListener("state-changed", () => {
329
- postState();
330
- });
331
- const deSyncDetectedSubscription = remoteSource.addEventListener("de-sync-detected", (event) => {
332
- broadcastNotification({
333
- notificationType: "de-sync-detected",
334
- reason: event.payload.reason
335
- });
336
- });
337
- const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener(
338
- "remote-schema-version-mismatch",
339
- (event) => {
340
- broadcastNotification({
341
- notificationType: "remote-schema-version-mismatch",
342
- remoteSchemaVersion: event.payload.remoteSchemaVersion,
343
- localSchemaVersion: event.payload.localSchemaVersion
344
- });
345
- }
346
- );
347
- const rpcTarget = {
348
- execute: (query) => db.execute(query),
349
- getSnapshot: () => {
350
- const appliedSyncId = getMaxSyncId(db, "pending");
351
- db.execute("PRAGMA journal_mode=off", { loggerLevel: "system" });
352
- const file = db.createSnapshot();
353
- db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
354
- return {
355
- file,
356
- syncId: appliedSyncId,
357
- schemaVersion: migrator.currentSchemaVersion
358
- };
359
- },
360
- importData: createImportData({
361
- migrator,
362
- tablesConfig: opts.syncDbSchema.tablesConfig,
363
- applyEvents: async (events) => {
364
- crdtStorage.applyOwnEvents(events);
365
- await crdtStorage.internal.processEnqueuedEvents();
366
- }
367
- }),
368
- postState,
369
- pushTabEvents: (request) => {
370
- const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);
371
- return {
372
- ok: true,
373
- beforeSyncId,
374
- afterSyncId
375
- };
376
- },
377
- pullEvents: (request) => {
378
- return crdtStorage.getEventsBatch({
379
- afterSyncId: request.afterSyncId,
380
- status: "applied",
381
- excludeNodeId: request.excludeNodeId ?? "",
382
- limit: 100
383
- });
384
- },
385
- goOnline: () => remoteSource.goOnline(),
386
- goOffline: () => remoteSource.goOffline("DISCONNECTED"),
387
- requestReload: createReloadRequestHandler({
388
- resetState,
389
- broadcast: broadcastNotification
390
- })
391
- };
392
- broadcastChannels.requests.onmessage = (event) => {
393
- const message = event.data;
394
- if (!isWorkerRequestMessage(message)) {
395
- return;
396
- }
397
- const sendError = (error) => {
398
- const response = {
399
- type: "error-response",
400
- requestId: message.requestId,
401
- error: error instanceof Error ? error.message : String(error)
402
- };
403
- broadcastChannels.responses.postMessage(response);
404
- };
405
- try {
406
- const method = rpcTarget[message.method];
407
- const data = method.apply(null, message.args);
408
- if (data instanceof Promise) {
409
- data.then((result) => {
410
- const response = {
411
- type: "response",
412
- requestId: message.requestId,
413
- data: result
414
- };
415
- broadcastChannels.responses.postMessage(response);
416
- }).catch(sendError);
417
- } else {
418
- const response = {
419
- type: "response",
420
- requestId: message.requestId,
421
- data
422
- };
423
- broadcastChannels.responses.postMessage(response);
424
- }
425
- } catch (error) {
426
- sendError(error);
427
- }
428
- };
429
- rpcTarget.postState();
430
- return async () => {
431
- stateChangedSubscription.unsubscribe();
432
- deSyncDetectedSubscription.unsubscribe();
433
- remoteSchemaVersionMismatchSubscription.unsubscribe();
434
- await remoteSource.dispose();
435
- broadcastChannels.requests.close();
436
- broadcastChannels.responses.close();
437
- db.close();
438
- };
172
+ const broadcastChannels = createBroadcastChannels(config.dbId);
173
+ const logger = opts.logger ?? defaultLogger;
174
+ if (opts.verifySchema) {
175
+ const issues = await verifySyncSchema(opts.syncDbSchema);
176
+ if (issues.length > 0) throw new Error(formatSchemaVerificationIssues(issues));
177
+ logger("worker", "Schema verification passed: migrations match the declared schema", "info");
178
+ }
179
+ const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);
180
+ const resetStore = opts.resetStore ?? createIdbResetStore();
181
+ const resetState = createResetStateStore({
182
+ store: resetStore,
183
+ dbId: config.dbId
184
+ });
185
+ const storageVersion = createStorageVersionStore({
186
+ store: resetStore,
187
+ dbId: config.dbId,
188
+ appStorageVersion: opts.storageVersion
189
+ });
190
+ const [pendingReset, isVersionMismatch] = await Promise.all([resetState.resolvePendingReset(), storageVersion.isVersionMismatch()]);
191
+ if (isVersionMismatch) logger("worker", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, "warning");
192
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
193
+ name: config.dbId,
194
+ directory: `.${config.dbId}`,
195
+ clearOnInit: !!pendingReset || isVersionMismatch,
196
+ initialCapacity: 8
197
+ });
198
+ const db = new SQLiteDbWrapper({
199
+ db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),
200
+ logger,
201
+ loggerPrefix: "worker",
202
+ sqlite3
203
+ });
204
+ db.execute("PRAGMA locking_mode=exclusive", { loggerLevel: "system" });
205
+ db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
206
+ db.execute("PRAGMA synchronous=NORMAL", { loggerLevel: "system" });
207
+ db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: "system" });
208
+ db.execute("PRAGMA worker.locking_mode=exclusive", { loggerLevel: "system" });
209
+ db.execute("PRAGMA worker.journal_mode=WAL", { loggerLevel: "system" });
210
+ db.execute("PRAGMA worker.synchronous=NORMAL", { loggerLevel: "system" });
211
+ const { kvStore } = applyWorkerDbSchema(db);
212
+ const migrator = createMigrator({
213
+ migrations: opts.syncDbSchema.migrations,
214
+ schemaVersion: kvStore.createNumberStoredValue("schema-version", -1),
215
+ updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier
216
+ });
217
+ migrator.migrateDbToLatest({ startTransaction: (callback) => {
218
+ db.executeTransaction((tx) => callback({ execute: (sql, parameters, meta) => tx.execute({
219
+ sql,
220
+ parameters
221
+ }, meta) }));
222
+ } });
223
+ db.invalidateDbSchema();
224
+ const pullSyncId = kvStore.createNumberStoredValue("pull-sync-id", -1);
225
+ const pushSyncId = kvStore.createNumberStoredValue("push-sync-id", -1);
226
+ const eventHlcAccumulator = kvStore.createStringStoredValue("crdt.consistency.event_hlc_sum.v2", "");
227
+ if (pendingReset) await resetState.markResetApplied(pendingReset.epoch);
228
+ if (isVersionMismatch) await storageVersion.markCurrentVersionApplied();
229
+ if (opts.eventLogGc === true) runWorkerEventLogGc({
230
+ db,
231
+ dbConfig: workerDbConfig,
232
+ pushSyncId,
233
+ eventHlcAccumulator,
234
+ logger
235
+ });
236
+ const crdtStorage = createCrdtStorage({
237
+ nodeId: config.clientId,
238
+ initialLocalSyncId: getMaxSyncId(db, "none"),
239
+ migrator,
240
+ hlc: new HLCCounter(config.clientId, () => Date.now()),
241
+ db,
242
+ dbConfig: workerDbConfig,
243
+ eventHlcAccumulator,
244
+ schema: opts.syncDbSchema
245
+ });
246
+ createCrdtSyncProducer({
247
+ storage: crdtStorage,
248
+ broadcastEvents: (chunk) => {
249
+ broadcastChannels.responses.postMessage({
250
+ notificationType: "new-event-chunk-applied",
251
+ newSyncId: chunk.newSyncId,
252
+ eventHlcSum: chunk.eventHlcSum
253
+ });
254
+ }
255
+ });
256
+ const remoteSource = createRemoteSource({
257
+ crdtStorage,
258
+ migrator,
259
+ clientId: config.clientId,
260
+ pullSyncId,
261
+ pushSyncId,
262
+ remoteFactory: opts.createRemoteSource
263
+ });
264
+ remoteSource.goOnline();
265
+ const broadcastNotification = (notification) => {
266
+ broadcastChannels.responses.postMessage(notification);
267
+ };
268
+ const postState = () => {
269
+ broadcastNotification({
270
+ notificationType: "state-changed",
271
+ state: remoteSource.getState()
272
+ });
273
+ };
274
+ const stateChangedSubscription = remoteSource.addEventListener("state-changed", () => {
275
+ postState();
276
+ });
277
+ const deSyncDetectedSubscription = remoteSource.addEventListener("de-sync-detected", (event) => {
278
+ broadcastNotification({
279
+ notificationType: "de-sync-detected",
280
+ reason: event.payload.reason
281
+ });
282
+ });
283
+ const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener("remote-schema-version-mismatch", (event) => {
284
+ broadcastNotification({
285
+ notificationType: "remote-schema-version-mismatch",
286
+ remoteSchemaVersion: event.payload.remoteSchemaVersion,
287
+ localSchemaVersion: event.payload.localSchemaVersion
288
+ });
289
+ });
290
+ const rpcTarget = {
291
+ execute: (query) => db.execute(query),
292
+ getSnapshot: () => {
293
+ const appliedSyncId = getMaxSyncId(db, "pending");
294
+ db.execute("PRAGMA journal_mode=off", { loggerLevel: "system" });
295
+ const file = db.createSnapshot();
296
+ db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
297
+ return {
298
+ file,
299
+ syncId: appliedSyncId,
300
+ schemaVersion: migrator.currentSchemaVersion
301
+ };
302
+ },
303
+ importData: createImportData({
304
+ migrator,
305
+ tablesConfig: opts.syncDbSchema.tablesConfig,
306
+ applyEvents: async (events) => {
307
+ crdtStorage.applyOwnEvents(events);
308
+ await crdtStorage.internal.processEnqueuedEvents();
309
+ }
310
+ }),
311
+ postState,
312
+ pushTabEvents: (request) => {
313
+ const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);
314
+ return {
315
+ ok: true,
316
+ beforeSyncId,
317
+ afterSyncId
318
+ };
319
+ },
320
+ pullEvents: (request) => {
321
+ return crdtStorage.getEventsBatch({
322
+ afterSyncId: request.afterSyncId,
323
+ status: "applied",
324
+ excludeNodeId: request.excludeNodeId ?? "",
325
+ limit: 100
326
+ });
327
+ },
328
+ goOnline: () => remoteSource.goOnline(),
329
+ goOffline: () => remoteSource.goOffline("DISCONNECTED"),
330
+ requestReload: createReloadRequestHandler({
331
+ resetState,
332
+ broadcast: broadcastNotification
333
+ })
334
+ };
335
+ broadcastChannels.requests.onmessage = (event) => {
336
+ const message = event.data;
337
+ if (!isWorkerRequestMessage(message)) return;
338
+ const sendError = (error) => {
339
+ const response = {
340
+ type: "error-response",
341
+ requestId: message.requestId,
342
+ error: error instanceof Error ? error.message : String(error)
343
+ };
344
+ broadcastChannels.responses.postMessage(response);
345
+ };
346
+ try {
347
+ const data = rpcTarget[message.method].apply(null, message.args);
348
+ if (data instanceof Promise) data.then((result) => {
349
+ const response = {
350
+ type: "response",
351
+ requestId: message.requestId,
352
+ data: result
353
+ };
354
+ broadcastChannels.responses.postMessage(response);
355
+ }).catch(sendError);
356
+ else {
357
+ const response = {
358
+ type: "response",
359
+ requestId: message.requestId,
360
+ data
361
+ };
362
+ broadcastChannels.responses.postMessage(response);
363
+ }
364
+ } catch (error) {
365
+ sendError(error);
366
+ }
367
+ };
368
+ rpcTarget.postState();
369
+ return async () => {
370
+ stateChangedSubscription.unsubscribe();
371
+ deSyncDetectedSubscription.unsubscribe();
372
+ remoteSchemaVersionMismatchSubscription.unsubscribe();
373
+ await remoteSource.dispose();
374
+ broadcastChannels.requests.close();
375
+ broadcastChannels.responses.close();
376
+ db.close();
377
+ };
439
378
  }
440
- function createRemoteSource({
441
- clientId,
442
- crdtStorage,
443
- migrator,
444
- pullSyncId,
445
- pushSyncId,
446
- remoteFactory
447
- }) {
448
- return createCrdtSyncRemoteSource({
449
- bufferSize: 50,
450
- pullSyncId,
451
- pushSyncId,
452
- nodeId: clientId,
453
- storage: crdtStorage,
454
- migrator,
455
- remoteFactory
456
- });
379
+ function createRemoteSource({ clientId, crdtStorage, migrator, pullSyncId, pushSyncId, remoteFactory }) {
380
+ return createCrdtSyncRemoteSource({
381
+ bufferSize: 50,
382
+ pullSyncId,
383
+ pushSyncId,
384
+ nodeId: clientId,
385
+ storage: crdtStorage,
386
+ migrator,
387
+ remoteFactory
388
+ });
457
389
  }
458
390
  async function getWorkerConfig() {
459
- let configSet = false;
460
- const responsePromise = createDeferredPromise();
461
- self.onmessage = (event) => {
462
- if (configSet) {
463
- console.error("Worker config already set");
464
- return;
465
- }
466
- const message = event.data;
467
- if (!isWorkerInitMessage(message)) {
468
- return;
469
- }
470
- responsePromise.resolve(message.config);
471
- configSet = true;
472
- };
473
- return responsePromise.promise;
391
+ let configSet = false;
392
+ const responsePromise = createDeferredPromise();
393
+ self.onmessage = (event) => {
394
+ if (configSet) {
395
+ console.error("Worker config already set");
396
+ return;
397
+ }
398
+ const message = event.data;
399
+ if (!isWorkerInitMessage(message)) return;
400
+ responsePromise.resolve(message.config);
401
+ configSet = true;
402
+ };
403
+ return responsePromise.promise;
474
404
  }
475
405
  async function startDbWorker(opts) {
476
- const config = opts.workerConfig ?? await getWorkerConfig();
477
- await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: "exclusive" }, async (lock) => {
478
- if (!lock) {
479
- return;
480
- }
481
- const cleanup = await createDbWorker(config, opts);
482
- const clientLockName = `${syncDbClientLockName}-${config.dbId}`;
483
- await new Promise((resolve) => {
484
- const interval = setInterval(async () => {
485
- const { held } = await navigator.locks.query();
486
- const hasClients = held?.some((l) => l.name === clientLockName && l.mode === "shared");
487
- if (!hasClients) {
488
- clearInterval(interval);
489
- resolve();
490
- }
491
- }, 5e3);
492
- });
493
- await cleanup();
494
- });
495
- self.close();
406
+ const config = opts.workerConfig ?? await getWorkerConfig();
407
+ await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: "exclusive" }, async (lock) => {
408
+ if (!lock) return;
409
+ const cleanup = await createDbWorker(config, opts);
410
+ const clientLockName = `${syncDbClientLockName}-${config.dbId}`;
411
+ await new Promise((resolve) => {
412
+ const interval = setInterval(async () => {
413
+ const { held } = await navigator.locks.query();
414
+ if (!held?.some((l) => l.name === clientLockName && l.mode === "shared")) {
415
+ clearInterval(interval);
416
+ resolve();
417
+ }
418
+ }, 5e3);
419
+ });
420
+ await cleanup();
421
+ });
422
+ self.close();
496
423
  }
497
424
  function getMaxSyncId(db, excludingStatus) {
498
- const [result] = db.executePrepared(
499
- "get-max-sync-id",
500
- { excludingStatus },
501
- (db2, params) => db2.selectFrom("worker.crdt_events").where("status", "!=", params("excludingStatus")).select((eb) => eb.fn.max("sync_id").as("sync_id")),
502
- { loggerLevel: "system" }
503
- );
504
- return result?.sync_id ?? 0;
425
+ const [result] = db.executePrepared("get-max-sync-id", { excludingStatus }, (db, params) => db.selectFrom("worker.crdt_events").where("status", "!=", params("excludingStatus")).select((eb) => eb.fn.max("sync_id").as("sync_id")), { loggerLevel: "system" });
426
+ return result?.sync_id ?? 0;
505
427
  }
506
- export {
507
- createWsRemoteSource,
508
- getWorkerConfig,
509
- startDbWorker
510
- };
428
+ //#endregion
429
+ export { createWsRemoteSource, getWorkerConfig, startDbWorker };
430
+
511
431
  //# sourceMappingURL=worker.js.map