@sqlite-sync/core 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/local.d.ts CHANGED
@@ -23,9 +23,9 @@ type LocalSyncedDbOptions<Database> = {
23
23
  */
24
24
  verifySchemaDriver?: () => SqliteDriver | Promise<SqliteDriver>;
25
25
  /**
26
- * Runs event-log garbage collection on startup. Disabled by default. GC keeps the
27
- * latest event rows and never removes pending events or local rows that have not
28
- * been pushed to the remote.
26
+ * Runs event-log garbage collection on startup. Enabled by default; set `false` to
27
+ * disable. GC keeps the latest event rows and never removes pending events or local
28
+ * rows that have not been pushed to the remote.
29
29
  */
30
30
  eventLogGc?: boolean;
31
31
  /** Runs once when the instance is disposed, including by `requestReload`. */
package/dist/local.js CHANGED
@@ -5,7 +5,7 @@ import { o as HLCCounter, t as createCrdtSyncRemoteSource, u as xxhash } from ".
5
5
  import { t as SQLiteReactiveDb } from "./sqlite-reactive-db-DLTvpGzO.js";
6
6
  import { a as makeCrdtTable, o as registerCrdtIntentDrainer } from "./make-crdt-table-Cu8WhRBk.js";
7
7
  import { n as getOrCreateSQLiteSyncDevtoolsRegistry, r as validateDbId, t as createSyncedDbDatabase } from "./synced-db-database-B5M7MrBD.js";
8
- import { i as runWorkerEventLogGc, r as formatStorageVersion, t as createWsRemoteSource } from "./ws-remote-source-BbPWrHB6.js";
8
+ import { i as runWorkerEventLogGc, r as formatStorageVersion, t as createWsRemoteSource } from "./ws-remote-source-CliRqkIW.js";
9
9
  //#region src/local/local-synced-db.ts
10
10
  const STORAGE_VERSION_KEY = "storage-version";
11
11
  const defaultLogger = (type, message, level = "info") => {
@@ -58,7 +58,7 @@ async function createLocalSyncedDb(options) {
58
58
  const stateHash = createKvStateHashStore(kvStore);
59
59
  const storageGeneration = kvStore.createStringStoredValue("storage-generation", "");
60
60
  if (!storageGeneration.current) storageGeneration.current = generateId();
61
- if (options.eventLogGc === true) runWorkerEventLogGc({
61
+ if (options.eventLogGc !== false) runWorkerEventLogGc({
62
62
  db,
63
63
  dbConfig: localDbConfig,
64
64
  pushSyncId,
package/dist/local.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"local.js","names":[],"sources":["../src/local/local-synced-db.ts"],"sourcesContent":["import { validateDbId } from \"../db-id\";\nimport { getOrCreateSQLiteSyncDevtoolsRegistry } from \"../devtools-registry\";\nimport { createExportData, createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { SQLiteReactiveDb } from \"../memory-db/sqlite-reactive-db\";\nimport { createMigrator } from \"../migrations/migrator\";\nimport {\n applyLocalDbSchema,\n LOCAL_KV_TABLE_NAME,\n type LocalDbSchema,\n localDbConfig,\n} from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues, verifySyncSchemaWithDriver } from \"../schema/verify-sync-schema\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n type CreateRemoteSourceFactory,\n createCrdtSyncRemoteSource,\n PUSH_BATCH_SIZE,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { makeCrdtTable, registerCrdtIntentDrainer } from \"../sqlite-crdt/make-crdt-table\";\nimport type { SqliteDriver } from \"../sqlite-driver\";\nimport { createKvStateHashStore, createKvStoreTableQuery, createSQLiteKvStore } from \"../sqlite-kv-store\";\nimport type { SyncedDb } from \"../sync-db\";\nimport { createSyncedDbDatabase } from \"../synced-db-database\";\nimport { createTypedEventTarget, generateId } from \"../utils\";\nimport { runWorkerEventLogGc } from \"../worker-db/event-log-gc\";\nimport { formatStorageVersion } from \"../worker-db/storage-version\";\nimport type { WorkerNotificationEvents, WorkerNotificationMessage } from \"../worker-db/worker-common\";\n\nexport type LocalSyncedDbOptions<Database> = {\n dbId: string;\n syncDbSchema: SyncDbSchema<Database>;\n /** Opens the persisted database. Called again after a reset deleted it. */\n openDriver: () => SqliteDriver | Promise<SqliteDriver>;\n /** Deletes the persisted database. Only called while no connection to it is open. */\n deleteDatabase: () => void | Promise<void>;\n createRemoteSource?: CreateRemoteSourceFactory;\n logger?: Logger;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the local DB is deleted on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * database opened by this callback and verifies the result matches the schema\n * declared with `defineSyncSchema`. On mismatch startup throws.\n */\n verifySchemaDriver?: () => SqliteDriver | Promise<SqliteDriver>;\n /**\n * Runs event-log garbage collection on startup. Disabled by default. GC keeps the\n * latest event rows and never removes pending events or local rows that have not\n * been pushed to the remote.\n */\n eventLogGc?: boolean;\n /** Runs once when the instance is disposed, including by `requestReload`. */\n onDispose?: () => void;\n};\n\nexport type LocalSyncedDb<Database> = SyncedDb<Database> & {\n state: SyncedDb<Database>[\"state\"] & {\n /** Drops the remote connection without retrying; call `sync` once the network is back. */\n dropConnection: () => void;\n };\n};\n\nconst STORAGE_VERSION_KEY = \"storage-version\";\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\n/**\n * A synced DB backed by a single persisted SQLite connection, for runtimes with one\n * JS context per database (React Native, Electron, Node). Reads and writes go to the\n * persisted database directly; there is no worker, tab coordination or in-memory copy.\n */\nexport async function createLocalSyncedDb<Database>(\n options: LocalSyncedDbOptions<Database>,\n): Promise<LocalSyncedDb<Database>> {\n validateDbId(options.dbId);\n const logger = options.logger ?? defaultLogger;\n\n if (options.verifySchemaDriver) {\n const issues = verifySyncSchemaWithDriver(options.syncDbSchema as SyncDbSchema, await options.verifySchemaDriver());\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"local\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n await xxhash.ensureLoaded();\n\n const reactiveDb = await openLocalReactiveDb(options, logger);\n const db = reactiveDb.db as unknown as SQLiteReactiveDb<LocalDbSchema>[\"db\"];\n\n try {\n const { kvStore } = applyLocalDbSchema(db);\n\n const migrator = createMigrator({\n migrations: options.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: localDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const stateHash = createKvStateHashStore(kvStore);\n const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\n\n if (options.eventLogGc === true) {\n runWorkerEventLogGc({ db, dbConfig: localDbConfig, pushSyncId, logger });\n }\n\n for (const table of options.syncDbSchema.tablesConfig) {\n makeCrdtTable({ db, baseTableName: table.baseTableName, crdtTableName: table.crdtTableName, temp: true });\n }\n\n const nodeId = generateId();\n const crdtStorage = createCrdtStorage({\n nodeId,\n initialLocalSyncId: Math.max(getMaxSyncId(db), pushSyncId.current),\n migrator,\n hlc: new HLCCounter(nodeId, () => Date.now()),\n db,\n dbConfig: localDbConfig,\n stateHash,\n schema: options.syncDbSchema,\n });\n registerCrdtIntentDrainer({ reactiveDb, storage: crdtStorage });\n\n kvStore.set(STORAGE_VERSION_KEY, formatStorageVersion(options.storageVersion));\n\n const eventTarget = createTypedEventTarget<WorkerNotificationEvents>();\n const notify = (notification: WorkerNotificationMessage) => {\n eventTarget.dispatchEvent(notification.notificationType, notification as never);\n };\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n notify({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n stateHash: chunk.stateHash,\n });\n },\n });\n\n const remoteSource = createCrdtSyncRemoteSource({\n bufferSize: PUSH_BATCH_SIZE,\n pullSyncId,\n pushSyncId,\n nodeId,\n storage: crdtStorage,\n migrator,\n remoteFactory: options.createRemoteSource,\n });\n\n let stateSnapshot = remoteSource.getState();\n remoteSource.addEventListener(\"state-changed\", () => {\n stateSnapshot = remoteSource.getState();\n notify({\n notificationType: \"state-changed\",\n state: stateSnapshot,\n storageGeneration: storageGeneration.current,\n });\n });\n remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n notify({ notificationType: \"de-sync-detected\", reason: event.payload.reason });\n });\n remoteSource.addEventListener(\"remote-schema-version-mismatch\", (event) => {\n notify({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n });\n remoteSource.goOnline();\n\n let unregisterDevtools: (() => void) | undefined;\n let disposePromise: Promise<void> | null = null;\n const dispose = () => {\n disposePromise ??= (async () => {\n options.onDispose?.();\n unregisterDevtools?.();\n try {\n await remoteSource.dispose();\n await crdtStorage.internal.processEnqueuedEvents();\n } finally {\n reactiveDb.dispose();\n }\n })();\n return disposePromise;\n };\n\n const importData = createImportData({\n migrator,\n tablesConfig: options.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n });\n\n const sync = () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Failed to start sync\", error));\n };\n\n const syncedDb: LocalSyncedDb<Database> = {\n db: createSyncedDbDatabase(reactiveDb),\n state: {\n getState: () => stateSnapshot,\n subscribe: (onChange: () => void) => {\n const { unsubscribe } = eventTarget.addEventListener(\"state-changed\", onChange);\n return unsubscribe;\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync,\n },\n /**\n * Disposes this instance and notifies `reload-requested` subscribers, which are\n * expected to create a new instance. With `clean: true` the persisted database is\n * deleted first, so the next instance starts empty and re-syncs from the remote.\n * Pending local events that were not pushed yet are lost with a clean reload.\n */\n requestReload: async ({ clean }: { clean: boolean }) => {\n await dispose();\n if (clean) {\n await options.deleteDatabase();\n }\n notify({ notificationType: \"reload-requested\", reloadEpoch: generateId(), clean });\n },\n subscribe: eventTarget.addEventListener,\n exportData: createExportData({\n reactiveDb,\n tablesConfig: options.syncDbSchema.tablesConfig,\n schemaVersion: migrator.currentSchemaVersion,\n }),\n importData: async (data, opts) => importData(data, opts),\n dispose,\n _internal: {\n executeAsync: async (query) => reactiveDb.db.execute(query),\n getMemoryQueryTables: reactiveDb.getTablesUsed.bind(reactiveDb),\n getSharedLiveQueriesSnapshot: reactiveDb.getSharedLiveQueriesSnapshot.bind(reactiveDb),\n crdtTableNames: options.syncDbSchema.tablesConfig.map((table) => table.crdtTableName),\n crdtTablesConfig: options.syncDbSchema.tablesConfig,\n tables: options.syncDbSchema.tables,\n schemaVersion: migrator.currentSchemaVersion,\n migrationVersions: Object.keys(options.syncDbSchema.migrations)\n .map(Number)\n .sort((a, b) => a - b),\n },\n };\n\n unregisterDevtools = getOrCreateSQLiteSyncDevtoolsRegistry().register({\n instanceId: generateId(),\n dbId: options.dbId,\n createdAt: Date.now(),\n instance: syncedDb,\n });\n\n return syncedDb;\n } catch (error) {\n reactiveDb.dispose();\n throw error;\n }\n}\n\nasync function openLocalReactiveDb<Database>(options: LocalSyncedDbOptions<Database>, logger: Logger) {\n const open = async () => {\n const driver = await options.openDriver();\n const reactiveDb = SQLiteReactiveDb.create<Database>({ driver, logger, loggerPrefix: \"local\" });\n try {\n reactiveDb.db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n reactiveDb.db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n reactiveDb.db.executeKysely((kysely) => createKvStoreTableQuery(kysely.schema, LOCAL_KV_TABLE_NAME), {\n loggerLevel: \"system\",\n });\n } catch (error) {\n reactiveDb.dispose();\n throw error;\n }\n return reactiveDb;\n };\n\n const reactiveDb = await open();\n const currentVersion = formatStorageVersion(options.storageVersion);\n const storedVersion = createSQLiteKvStore({ db: reactiveDb.db, metaTableName: LOCAL_KV_TABLE_NAME }).get(\n STORAGE_VERSION_KEY,\n );\n if (storedVersion === null || storedVersion === currentVersion) {\n return reactiveDb;\n }\n\n logger(\"local\", `Storage version mismatch — resetting local DB to ${currentVersion}`, \"warning\");\n reactiveDb.dispose();\n await options.deleteDatabase();\n return open();\n}\n\nfunction getMaxSyncId(db: SQLiteReactiveDb<LocalDbSchema>[\"db\"]) {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n {},\n (db) => db.selectFrom(\"crdt_events\").select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"local.js","names":[],"sources":["../src/local/local-synced-db.ts"],"sourcesContent":["import { validateDbId } from \"../db-id\";\nimport { getOrCreateSQLiteSyncDevtoolsRegistry } from \"../devtools-registry\";\nimport { createExportData, createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { SQLiteReactiveDb } from \"../memory-db/sqlite-reactive-db\";\nimport { createMigrator } from \"../migrations/migrator\";\nimport {\n applyLocalDbSchema,\n LOCAL_KV_TABLE_NAME,\n type LocalDbSchema,\n localDbConfig,\n} from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues, verifySyncSchemaWithDriver } from \"../schema/verify-sync-schema\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n type CreateRemoteSourceFactory,\n createCrdtSyncRemoteSource,\n PUSH_BATCH_SIZE,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport { makeCrdtTable, registerCrdtIntentDrainer } from \"../sqlite-crdt/make-crdt-table\";\nimport type { SqliteDriver } from \"../sqlite-driver\";\nimport { createKvStateHashStore, createKvStoreTableQuery, createSQLiteKvStore } from \"../sqlite-kv-store\";\nimport type { SyncedDb } from \"../sync-db\";\nimport { createSyncedDbDatabase } from \"../synced-db-database\";\nimport { createTypedEventTarget, generateId } from \"../utils\";\nimport { runWorkerEventLogGc } from \"../worker-db/event-log-gc\";\nimport { formatStorageVersion } from \"../worker-db/storage-version\";\nimport type { WorkerNotificationEvents, WorkerNotificationMessage } from \"../worker-db/worker-common\";\n\nexport type LocalSyncedDbOptions<Database> = {\n dbId: string;\n syncDbSchema: SyncDbSchema<Database>;\n /** Opens the persisted database. Called again after a reset deleted it. */\n openDriver: () => SqliteDriver | Promise<SqliteDriver>;\n /** Deletes the persisted database. Only called while no connection to it is open. */\n deleteDatabase: () => void | Promise<void>;\n createRemoteSource?: CreateRemoteSourceFactory;\n logger?: Logger;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the local DB is deleted on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * database opened by this callback and verifies the result matches the schema\n * declared with `defineSyncSchema`. On mismatch startup throws.\n */\n verifySchemaDriver?: () => SqliteDriver | Promise<SqliteDriver>;\n /**\n * Runs event-log garbage collection on startup. Enabled by default; set `false` to\n * disable. GC keeps the latest event rows and never removes pending events or local\n * rows that have not been pushed to the remote.\n */\n eventLogGc?: boolean;\n /** Runs once when the instance is disposed, including by `requestReload`. */\n onDispose?: () => void;\n};\n\nexport type LocalSyncedDb<Database> = SyncedDb<Database> & {\n state: SyncedDb<Database>[\"state\"] & {\n /** Drops the remote connection without retrying; call `sync` once the network is back. */\n dropConnection: () => void;\n };\n};\n\nconst STORAGE_VERSION_KEY = \"storage-version\";\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\n/**\n * A synced DB backed by a single persisted SQLite connection, for runtimes with one\n * JS context per database (React Native, Electron, Node). Reads and writes go to the\n * persisted database directly; there is no worker, tab coordination or in-memory copy.\n */\nexport async function createLocalSyncedDb<Database>(\n options: LocalSyncedDbOptions<Database>,\n): Promise<LocalSyncedDb<Database>> {\n validateDbId(options.dbId);\n const logger = options.logger ?? defaultLogger;\n\n if (options.verifySchemaDriver) {\n const issues = verifySyncSchemaWithDriver(options.syncDbSchema as SyncDbSchema, await options.verifySchemaDriver());\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"local\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n await xxhash.ensureLoaded();\n\n const reactiveDb = await openLocalReactiveDb(options, logger);\n const db = reactiveDb.db as unknown as SQLiteReactiveDb<LocalDbSchema>[\"db\"];\n\n try {\n const { kvStore } = applyLocalDbSchema(db);\n\n const migrator = createMigrator({\n migrations: options.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: localDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const stateHash = createKvStateHashStore(kvStore);\n const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\n\n if (options.eventLogGc !== false) {\n runWorkerEventLogGc({ db, dbConfig: localDbConfig, pushSyncId, logger });\n }\n\n for (const table of options.syncDbSchema.tablesConfig) {\n makeCrdtTable({ db, baseTableName: table.baseTableName, crdtTableName: table.crdtTableName, temp: true });\n }\n\n const nodeId = generateId();\n const crdtStorage = createCrdtStorage({\n nodeId,\n initialLocalSyncId: Math.max(getMaxSyncId(db), pushSyncId.current),\n migrator,\n hlc: new HLCCounter(nodeId, () => Date.now()),\n db,\n dbConfig: localDbConfig,\n stateHash,\n schema: options.syncDbSchema,\n });\n registerCrdtIntentDrainer({ reactiveDb, storage: crdtStorage });\n\n kvStore.set(STORAGE_VERSION_KEY, formatStorageVersion(options.storageVersion));\n\n const eventTarget = createTypedEventTarget<WorkerNotificationEvents>();\n const notify = (notification: WorkerNotificationMessage) => {\n eventTarget.dispatchEvent(notification.notificationType, notification as never);\n };\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n notify({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n stateHash: chunk.stateHash,\n });\n },\n });\n\n const remoteSource = createCrdtSyncRemoteSource({\n bufferSize: PUSH_BATCH_SIZE,\n pullSyncId,\n pushSyncId,\n nodeId,\n storage: crdtStorage,\n migrator,\n remoteFactory: options.createRemoteSource,\n });\n\n let stateSnapshot = remoteSource.getState();\n remoteSource.addEventListener(\"state-changed\", () => {\n stateSnapshot = remoteSource.getState();\n notify({\n notificationType: \"state-changed\",\n state: stateSnapshot,\n storageGeneration: storageGeneration.current,\n });\n });\n remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n notify({ notificationType: \"de-sync-detected\", reason: event.payload.reason });\n });\n remoteSource.addEventListener(\"remote-schema-version-mismatch\", (event) => {\n notify({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n });\n remoteSource.goOnline();\n\n let unregisterDevtools: (() => void) | undefined;\n let disposePromise: Promise<void> | null = null;\n const dispose = () => {\n disposePromise ??= (async () => {\n options.onDispose?.();\n unregisterDevtools?.();\n try {\n await remoteSource.dispose();\n await crdtStorage.internal.processEnqueuedEvents();\n } finally {\n reactiveDb.dispose();\n }\n })();\n return disposePromise;\n };\n\n const importData = createImportData({\n migrator,\n tablesConfig: options.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n });\n\n const sync = () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Failed to start sync\", error));\n };\n\n const syncedDb: LocalSyncedDb<Database> = {\n db: createSyncedDbDatabase(reactiveDb),\n state: {\n getState: () => stateSnapshot,\n subscribe: (onChange: () => void) => {\n const { unsubscribe } = eventTarget.addEventListener(\"state-changed\", onChange);\n return unsubscribe;\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync,\n },\n /**\n * Disposes this instance and notifies `reload-requested` subscribers, which are\n * expected to create a new instance. With `clean: true` the persisted database is\n * deleted first, so the next instance starts empty and re-syncs from the remote.\n * Pending local events that were not pushed yet are lost with a clean reload.\n */\n requestReload: async ({ clean }: { clean: boolean }) => {\n await dispose();\n if (clean) {\n await options.deleteDatabase();\n }\n notify({ notificationType: \"reload-requested\", reloadEpoch: generateId(), clean });\n },\n subscribe: eventTarget.addEventListener,\n exportData: createExportData({\n reactiveDb,\n tablesConfig: options.syncDbSchema.tablesConfig,\n schemaVersion: migrator.currentSchemaVersion,\n }),\n importData: async (data, opts) => importData(data, opts),\n dispose,\n _internal: {\n executeAsync: async (query) => reactiveDb.db.execute(query),\n getMemoryQueryTables: reactiveDb.getTablesUsed.bind(reactiveDb),\n getSharedLiveQueriesSnapshot: reactiveDb.getSharedLiveQueriesSnapshot.bind(reactiveDb),\n crdtTableNames: options.syncDbSchema.tablesConfig.map((table) => table.crdtTableName),\n crdtTablesConfig: options.syncDbSchema.tablesConfig,\n tables: options.syncDbSchema.tables,\n schemaVersion: migrator.currentSchemaVersion,\n migrationVersions: Object.keys(options.syncDbSchema.migrations)\n .map(Number)\n .sort((a, b) => a - b),\n },\n };\n\n unregisterDevtools = getOrCreateSQLiteSyncDevtoolsRegistry().register({\n instanceId: generateId(),\n dbId: options.dbId,\n createdAt: Date.now(),\n instance: syncedDb,\n });\n\n return syncedDb;\n } catch (error) {\n reactiveDb.dispose();\n throw error;\n }\n}\n\nasync function openLocalReactiveDb<Database>(options: LocalSyncedDbOptions<Database>, logger: Logger) {\n const open = async () => {\n const driver = await options.openDriver();\n const reactiveDb = SQLiteReactiveDb.create<Database>({ driver, logger, loggerPrefix: \"local\" });\n try {\n reactiveDb.db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n reactiveDb.db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n reactiveDb.db.executeKysely((kysely) => createKvStoreTableQuery(kysely.schema, LOCAL_KV_TABLE_NAME), {\n loggerLevel: \"system\",\n });\n } catch (error) {\n reactiveDb.dispose();\n throw error;\n }\n return reactiveDb;\n };\n\n const reactiveDb = await open();\n const currentVersion = formatStorageVersion(options.storageVersion);\n const storedVersion = createSQLiteKvStore({ db: reactiveDb.db, metaTableName: LOCAL_KV_TABLE_NAME }).get(\n STORAGE_VERSION_KEY,\n );\n if (storedVersion === null || storedVersion === currentVersion) {\n return reactiveDb;\n }\n\n logger(\"local\", `Storage version mismatch — resetting local DB to ${currentVersion}`, \"warning\");\n reactiveDb.dispose();\n await options.deleteDatabase();\n return open();\n}\n\nfunction getMaxSyncId(db: SQLiteReactiveDb<LocalDbSchema>[\"db\"]) {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n {},\n (db) => db.selectFrom(\"crdt_events\").select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
package/dist/worker.d.ts CHANGED
@@ -23,8 +23,8 @@ type WorkerOptions = {
23
23
  */
24
24
  verifySchema?: boolean;
25
25
  /**
26
- * Runs local worker event-log garbage collection on startup. Disabled by default;
27
- * set `eventLogGc: true` to enable. GC keeps the latest event rows and never removes
26
+ * Runs local worker event-log garbage collection on startup. Enabled by default;
27
+ * set `eventLogGc: false` to disable. GC keeps the latest event rows and never removes
28
28
  * pending events or local rows that have not been pushed to the remote.
29
29
  * Skipped while an existing tab holds an in-memory replica of this DB.
30
30
  */
package/dist/worker.js CHANGED
@@ -3,7 +3,7 @@ import { l as generateId, o as createDeferredPromise } from "./crdt-table-schema
3
3
  import { a as formatSchemaVerificationIssues, c as createMigrator, n as createCrdtStorage, t as createCrdtSyncProducer, u as createImportData } from "./crdt-sync-producer-BQkuzCBM.js";
4
4
  import { o as HLCCounter, t as createCrdtSyncRemoteSource, u as xxhash } from "./crdt-sync-remote-source-yeTEkOjH.js";
5
5
  import { _ as createWasmSqliteDriver, d as isWorkerRequestMessage, h as verifySyncSchema, i as createResetStateStore, l as isWorkerInitMessage, m as syncDbWorkerLockName, n as createIdbResetStore, o as runWithNoActiveReplicas, p as syncDbClientLockName, r as createReloadRequestHandler, s as createBroadcastChannels } from "./reset-state-DAPyJglN.js";
6
- import { i as runWorkerEventLogGc, n as createStorageVersionStore, t as createWsRemoteSource } from "./ws-remote-source-BbPWrHB6.js";
6
+ import { i as runWorkerEventLogGc, n as createStorageVersionStore, t as createWsRemoteSource } from "./ws-remote-source-CliRqkIW.js";
7
7
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
8
8
  //#region src/worker-db/db-worker.ts
9
9
  const defaultLogger = (type, message, level = "info") => {
@@ -82,7 +82,7 @@ async function createDbWorker(config, opts) {
82
82
  if (!storageGeneration.current) storageGeneration.current = generateId();
83
83
  if (pendingReset) await resetState.markResetApplied(pendingReset.epoch);
84
84
  if (isVersionMismatch) await storageVersion.markCurrentVersionApplied();
85
- if (opts.eventLogGc === true) await runWithNoActiveReplicas({
85
+ if (opts.eventLogGc !== false) await runWithNoActiveReplicas({
86
86
  dbId: config.dbId,
87
87
  run: () => {
88
88
  runWorkerEventLogGc({
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","names":[],"sources":["../src/worker-db/db-worker.ts"],"sourcesContent":["import sqlite3InitModule from \"@sqlite.org/sqlite-wasm\";\nimport { createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { createMigrator, type SyncDbMigrator } from \"../migrations/migrator\";\nimport { applyWorkerDbSchema, type WorkerDbSchema, workerDbConfig } from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues } from \"../schema/verify-sync-schema\";\nimport { verifySyncSchema } from \"../schema/verify-sync-schema-wasm\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { type CrdtStorage, createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n type CreateRemoteSourceFactory,\n createCrdtSyncRemoteSource,\n PUSH_BATCH_SIZE,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { CrdtEventStatus } from \"../sqlite-crdt/crdt-table-schema\";\nimport { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport { createKvStateHashStore, type KvStore } from \"../sqlite-kv-store\";\nimport { createDeferredPromise, generateId } from \"../utils\";\nimport { createWasmSqliteDriver } from \"../wasm-driver\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { runWithNoActiveReplicas } from \"./replica-lock\";\nimport { createIdbResetStore, createReloadRequestHandler, createResetStateStore, type ResetStore } from \"./reset-state\";\nimport { createStorageVersionStore } from \"./storage-version\";\nimport {\n createBroadcastChannels,\n isWorkerInitMessage,\n isWorkerRequestMessage,\n syncDbClientLockName,\n syncDbWorkerLockName,\n type WorkerConfig,\n type WorkerErrorResponseMessage,\n type WorkerNotificationMessage,\n type WorkerResponseMessage,\n type WorkerRpc,\n} from \"./worker-common\";\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\nasync function createDbWorker(config: WorkerConfig, opts: WorkerOptions) {\n const broadcastChannels = createBroadcastChannels(config.dbId);\n const logger = opts.logger ?? defaultLogger;\n\n if (opts.verifySchema) {\n const issues = await verifySyncSchema(opts.syncDbSchema);\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"worker\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);\n\n const resetStore = opts.resetStore ?? createIdbResetStore();\n const resetState = createResetStateStore({\n store: resetStore,\n dbId: config.dbId,\n });\n const storageVersion = createStorageVersionStore({\n store: resetStore,\n dbId: config.dbId,\n appStorageVersion: opts.storageVersion,\n });\n const [pendingReset, versionStatus] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.getVersionStatus(),\n ]);\n const isVersionMismatch = versionStatus !== \"current\";\n\n if (versionStatus === \"missing\") {\n logger(\"worker\", `Initializing local DB storage version ${storageVersion.currentVersion}`, \"info\");\n } else if (versionStatus === \"outdated\") {\n logger(\"worker\", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, \"warning\");\n }\n\n const pool = await sqlite3.installOpfsSAHPoolVfs({\n name: config.dbId,\n directory: `.${config.dbId}`,\n clearOnInit: !!pendingReset || isVersionMismatch,\n initialCapacity: 8,\n });\n\n const db = new SQLiteDbWrapper<WorkerDbSchema>({\n driver: createWasmSqliteDriver(sqlite3, new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`)),\n logger: logger,\n loggerPrefix: \"worker\",\n });\n\n db.execute(\"PRAGMA locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n const { kvStore } = applyWorkerDbSchema(db);\n\n const migrator = createMigrator({\n migrations: opts.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const stateHash = createKvStateHashStore(kvStore);\n const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\n\n // Record the applied reset epoch / storage version only after the wiped DB\n // initialized successfully, so a failed init can be retried by a later\n // elected worker, while a later election does not wipe again.\n if (pendingReset) {\n await resetState.markResetApplied(pendingReset.epoch);\n }\n if (isVersionMismatch) {\n await storageVersion.markCurrentVersionApplied();\n }\n\n if (opts.eventLogGc === true) {\n await runWithNoActiveReplicas({\n dbId: config.dbId,\n run: () => {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n logger,\n });\n },\n });\n }\n\n const crdtStorage = createCrdtStorage({\n nodeId: config.clientId,\n initialLocalSyncId: Math.max(getMaxSyncId(db, \"none\"), pushSyncId.current),\n migrator,\n hlc: new HLCCounter(config.clientId, () => Date.now()),\n db,\n dbConfig: workerDbConfig,\n stateHash,\n schema: opts.syncDbSchema,\n });\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n broadcastChannels.responses.postMessage({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n stateHash: chunk.stateHash,\n });\n },\n });\n\n const remoteSource = createRemoteSource({\n crdtStorage,\n migrator,\n clientId: config.clientId,\n pullSyncId,\n pushSyncId,\n remoteFactory: opts.createRemoteSource,\n });\n remoteSource.goOnline();\n\n const broadcastNotification = (notification: WorkerNotificationMessage) => {\n broadcastChannels.responses.postMessage(notification);\n };\n\n const postState = () => {\n broadcastNotification({\n notificationType: \"state-changed\",\n state: remoteSource.getState(),\n storageGeneration: storageGeneration.current,\n });\n };\n const stateChangedSubscription = remoteSource.addEventListener(\"state-changed\", () => {\n postState();\n });\n const deSyncDetectedSubscription = remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n broadcastNotification({\n notificationType: \"de-sync-detected\",\n reason: event.payload.reason,\n });\n });\n const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener(\n \"remote-schema-version-mismatch\",\n (event) => {\n broadcastNotification({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n },\n );\n\n const rpcTarget: WorkerRpc = {\n execute: (query) => db.execute(query),\n getSnapshot: () => {\n const appliedSyncId = getMaxSyncId(db, \"pending\");\n db.execute(\"PRAGMA journal_mode=off\", { loggerLevel: \"system\" });\n const file = db.createSnapshot();\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n return {\n file,\n syncId: appliedSyncId,\n schemaVersion: migrator.currentSchemaVersion,\n storageGeneration: storageGeneration.current,\n };\n },\n importData: createImportData({\n migrator,\n tablesConfig: opts.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n }),\n postState,\n pushTabEvents: (request) => {\n const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);\n return {\n ok: true,\n beforeSyncId,\n afterSyncId,\n };\n },\n pullEvents: (request) => {\n return crdtStorage.getEventsBatch({\n afterSyncId: request.afterSyncId,\n status: \"applied\",\n excludeNodeId: request.excludeNodeId ?? \"\",\n limit: 100,\n });\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync: () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Sync failed\", error));\n },\n requestReload: createReloadRequestHandler({\n resetState,\n broadcast: broadcastNotification,\n }),\n };\n\n broadcastChannels.requests.onmessage = (event) => {\n const message = event.data;\n\n if (!isWorkerRequestMessage(message)) {\n return;\n }\n\n const sendError = (error: unknown) => {\n const response: WorkerErrorResponseMessage = {\n type: \"error-response\",\n requestId: message.requestId,\n error: error instanceof Error ? error.message : String(error),\n };\n broadcastChannels.responses.postMessage(response);\n };\n\n try {\n const method = rpcTarget[message.method] as () => ReturnType<WorkerRpc[keyof WorkerRpc]>;\n const data = method.apply(null, message.args as []);\n\n if (data instanceof Promise) {\n data\n .then((result) => {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data: result,\n };\n broadcastChannels.responses.postMessage(response);\n })\n .catch(sendError);\n } else {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data,\n };\n broadcastChannels.responses.postMessage(response);\n }\n } catch (error) {\n sendError(error);\n }\n };\n\n rpcTarget.postState();\n\n return async () => {\n stateChangedSubscription.unsubscribe();\n deSyncDetectedSubscription.unsubscribe();\n remoteSchemaVersionMismatchSubscription.unsubscribe();\n await remoteSource.dispose();\n broadcastChannels.requests.close();\n broadcastChannels.responses.close();\n db.close();\n };\n}\n\ntype InitRemoteOptions = {\n clientId: string;\n crdtStorage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n pushSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nfunction createRemoteSource({\n clientId,\n crdtStorage,\n migrator,\n pullSyncId,\n pushSyncId,\n remoteFactory,\n}: InitRemoteOptions) {\n return createCrdtSyncRemoteSource({\n bufferSize: PUSH_BATCH_SIZE,\n pullSyncId,\n pushSyncId,\n nodeId: clientId,\n storage: crdtStorage,\n migrator,\n remoteFactory,\n });\n}\n\nexport async function getWorkerConfig<Props = never>(): Promise<WorkerConfig<Props>> {\n let configSet = false;\n const responsePromise = createDeferredPromise<WorkerConfig>();\n\n self.onmessage = (event: MessageEvent<unknown>) => {\n if (configSet) {\n console.error(\"Worker config already set\");\n return;\n }\n\n const message = event.data;\n if (!isWorkerInitMessage(message)) {\n return;\n }\n\n responsePromise.resolve(message.config);\n configSet = true;\n };\n\n return responsePromise.promise;\n}\n\ntype WorkerOptions = {\n syncDbSchema: SyncDbSchema;\n logger?: Logger;\n createRemoteSource?: CreateRemoteSourceFactory;\n workerConfig?: WorkerConfig;\n /** Durable storage for reset state. Defaults to an IndexedDB-backed store. */\n resetStore?: ResetStore;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the elected worker wipes the local DB on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * in-memory database and verifies the result matches the schema declared with\n * `defineSyncSchema`. On mismatch the worker throws and refuses to start.\n * Enable in development only, e.g. `verifySchema: import.meta.env.DEV`.\n */\n verifySchema?: boolean;\n /**\n * Runs local worker event-log garbage collection on startup. Disabled by default;\n * set `eventLogGc: true` to enable. GC keeps the latest event rows and never removes\n * pending events or local rows that have not been pushed to the remote.\n * Skipped while an existing tab holds an in-memory replica of this DB.\n */\n eventLogGc?: boolean;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = opts.workerConfig ?? (await getWorkerConfig());\n\n await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: \"exclusive\" }, async (lock) => {\n if (!lock) {\n return;\n }\n\n const cleanup = await createDbWorker(config, opts);\n\n const clientLockName = `${syncDbClientLockName}-${config.dbId}`;\n await new Promise<void>((resolve) => {\n const interval = setInterval(async () => {\n const { held } = await navigator.locks.query();\n const hasClients = held?.some((l) => l.name === clientLockName && l.mode === \"shared\");\n if (!hasClients) {\n clearInterval(interval);\n resolve();\n }\n }, 5_000);\n });\n\n await cleanup();\n });\n\n self.close();\n}\n\nfunction getMaxSyncId(db: SQLiteDbWrapper<WorkerDbSchema>, excludingStatus: \"none\" | \"pending\") {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n { excludingStatus: excludingStatus as CrdtEventStatus },\n (db, params) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"!=\", params(\"excludingStatus\"))\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/worker-db/db-worker.ts"],"sourcesContent":["import sqlite3InitModule from \"@sqlite.org/sqlite-wasm\";\nimport { createImportData } from \"../export-import\";\nimport { xxhash } from \"../hash\";\nimport { HLCCounter } from \"../hlc\";\nimport type { Logger } from \"../logger\";\nimport { createMigrator, type SyncDbMigrator } from \"../migrations/migrator\";\nimport { applyWorkerDbSchema, type WorkerDbSchema, workerDbConfig } from \"../migrations/system-schema\";\nimport { formatSchemaVerificationIssues } from \"../schema/verify-sync-schema\";\nimport { verifySyncSchema } from \"../schema/verify-sync-schema-wasm\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { type CrdtStorage, createCrdtStorage } from \"../sqlite-crdt/crdt-storage\";\nimport { createCrdtSyncProducer } from \"../sqlite-crdt/crdt-sync-producer\";\nimport {\n type CreateRemoteSourceFactory,\n createCrdtSyncRemoteSource,\n PUSH_BATCH_SIZE,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { CrdtEventStatus } from \"../sqlite-crdt/crdt-table-schema\";\nimport { SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport { createKvStateHashStore, type KvStore } from \"../sqlite-kv-store\";\nimport { createDeferredPromise, generateId } from \"../utils\";\nimport { createWasmSqliteDriver } from \"../wasm-driver\";\nimport { runWorkerEventLogGc } from \"./event-log-gc\";\nimport { runWithNoActiveReplicas } from \"./replica-lock\";\nimport { createIdbResetStore, createReloadRequestHandler, createResetStateStore, type ResetStore } from \"./reset-state\";\nimport { createStorageVersionStore } from \"./storage-version\";\nimport {\n createBroadcastChannels,\n isWorkerInitMessage,\n isWorkerRequestMessage,\n syncDbClientLockName,\n syncDbWorkerLockName,\n type WorkerConfig,\n type WorkerErrorResponseMessage,\n type WorkerNotificationMessage,\n type WorkerResponseMessage,\n type WorkerRpc,\n} from \"./worker-common\";\n\nconst defaultLogger: Logger = (type, message, level = \"info\") => {\n const logMessage = `[${type}] ${message}`;\n switch (level) {\n case \"info\":\n console.log(logMessage);\n break;\n case \"warning\":\n console.warn(logMessage);\n break;\n case \"error\":\n console.error(logMessage);\n break;\n case \"trace\":\n console.trace(logMessage);\n break;\n }\n};\n\nasync function createDbWorker(config: WorkerConfig, opts: WorkerOptions) {\n const broadcastChannels = createBroadcastChannels(config.dbId);\n const logger = opts.logger ?? defaultLogger;\n\n if (opts.verifySchema) {\n const issues = await verifySyncSchema(opts.syncDbSchema);\n if (issues.length > 0) {\n throw new Error(formatSchemaVerificationIssues(issues));\n }\n logger(\"worker\", \"Schema verification passed: migrations match the declared schema\", \"info\");\n }\n\n const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);\n\n const resetStore = opts.resetStore ?? createIdbResetStore();\n const resetState = createResetStateStore({\n store: resetStore,\n dbId: config.dbId,\n });\n const storageVersion = createStorageVersionStore({\n store: resetStore,\n dbId: config.dbId,\n appStorageVersion: opts.storageVersion,\n });\n const [pendingReset, versionStatus] = await Promise.all([\n resetState.resolvePendingReset(),\n storageVersion.getVersionStatus(),\n ]);\n const isVersionMismatch = versionStatus !== \"current\";\n\n if (versionStatus === \"missing\") {\n logger(\"worker\", `Initializing local DB storage version ${storageVersion.currentVersion}`, \"info\");\n } else if (versionStatus === \"outdated\") {\n logger(\"worker\", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, \"warning\");\n }\n\n const pool = await sqlite3.installOpfsSAHPoolVfs({\n name: config.dbId,\n directory: `.${config.dbId}`,\n clearOnInit: !!pendingReset || isVersionMismatch,\n initialCapacity: 8,\n });\n\n const db = new SQLiteDbWrapper<WorkerDbSchema>({\n driver: createWasmSqliteDriver(sqlite3, new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`)),\n logger: logger,\n loggerPrefix: \"worker\",\n });\n\n db.execute(\"PRAGMA locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.locking_mode=exclusive\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.journal_mode=WAL\", { loggerLevel: \"system\" });\n db.execute(\"PRAGMA worker.synchronous=NORMAL\", { loggerLevel: \"system\" });\n\n const { kvStore } = applyWorkerDbSchema(db);\n\n const migrator = createMigrator({\n migrations: opts.syncDbSchema.migrations,\n schemaVersion: kvStore.createNumberStoredValue(\"schema-version\", -1),\n updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier,\n });\n migrator.migrateDbToLatest({\n startTransaction: (callback) => {\n db.executeTransaction((tx) =>\n callback({ execute: (sql, parameters, meta) => tx.execute({ sql, parameters }, meta) }),\n );\n },\n });\n db.invalidateDbSchema();\n\n const pullSyncId = kvStore.createNumberStoredValue(\"pull-sync-id\", -1);\n const pushSyncId = kvStore.createNumberStoredValue(\"push-sync-id\", -1);\n const stateHash = createKvStateHashStore(kvStore);\n const storageGeneration = kvStore.createStringStoredValue(\"storage-generation\", \"\");\n if (!storageGeneration.current) {\n storageGeneration.current = generateId();\n }\n\n // Record the applied reset epoch / storage version only after the wiped DB\n // initialized successfully, so a failed init can be retried by a later\n // elected worker, while a later election does not wipe again.\n if (pendingReset) {\n await resetState.markResetApplied(pendingReset.epoch);\n }\n if (isVersionMismatch) {\n await storageVersion.markCurrentVersionApplied();\n }\n\n if (opts.eventLogGc !== false) {\n await runWithNoActiveReplicas({\n dbId: config.dbId,\n run: () => {\n runWorkerEventLogGc({\n db,\n dbConfig: workerDbConfig,\n pushSyncId,\n logger,\n });\n },\n });\n }\n\n const crdtStorage = createCrdtStorage({\n nodeId: config.clientId,\n initialLocalSyncId: Math.max(getMaxSyncId(db, \"none\"), pushSyncId.current),\n migrator,\n hlc: new HLCCounter(config.clientId, () => Date.now()),\n db,\n dbConfig: workerDbConfig,\n stateHash,\n schema: opts.syncDbSchema,\n });\n\n createCrdtSyncProducer({\n storage: crdtStorage,\n broadcastEvents: (chunk) => {\n broadcastChannels.responses.postMessage({\n notificationType: \"new-event-chunk-applied\",\n newSyncId: chunk.newSyncId,\n stateHash: chunk.stateHash,\n });\n },\n });\n\n const remoteSource = createRemoteSource({\n crdtStorage,\n migrator,\n clientId: config.clientId,\n pullSyncId,\n pushSyncId,\n remoteFactory: opts.createRemoteSource,\n });\n remoteSource.goOnline();\n\n const broadcastNotification = (notification: WorkerNotificationMessage) => {\n broadcastChannels.responses.postMessage(notification);\n };\n\n const postState = () => {\n broadcastNotification({\n notificationType: \"state-changed\",\n state: remoteSource.getState(),\n storageGeneration: storageGeneration.current,\n });\n };\n const stateChangedSubscription = remoteSource.addEventListener(\"state-changed\", () => {\n postState();\n });\n const deSyncDetectedSubscription = remoteSource.addEventListener(\"de-sync-detected\", (event) => {\n broadcastNotification({\n notificationType: \"de-sync-detected\",\n reason: event.payload.reason,\n });\n });\n const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener(\n \"remote-schema-version-mismatch\",\n (event) => {\n broadcastNotification({\n notificationType: \"remote-schema-version-mismatch\",\n remoteSchemaVersion: event.payload.remoteSchemaVersion,\n localSchemaVersion: event.payload.localSchemaVersion,\n });\n },\n );\n\n const rpcTarget: WorkerRpc = {\n execute: (query) => db.execute(query),\n getSnapshot: () => {\n const appliedSyncId = getMaxSyncId(db, \"pending\");\n db.execute(\"PRAGMA journal_mode=off\", { loggerLevel: \"system\" });\n const file = db.createSnapshot();\n db.execute(\"PRAGMA journal_mode=WAL\", { loggerLevel: \"system\" });\n return {\n file,\n syncId: appliedSyncId,\n schemaVersion: migrator.currentSchemaVersion,\n storageGeneration: storageGeneration.current,\n };\n },\n importData: createImportData({\n migrator,\n tablesConfig: opts.syncDbSchema.tablesConfig,\n applyEvents: async (events) => {\n crdtStorage.applyOwnEvents(events);\n await crdtStorage.internal.processEnqueuedEvents();\n },\n }),\n postState,\n pushTabEvents: (request) => {\n const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);\n return {\n ok: true,\n beforeSyncId,\n afterSyncId,\n };\n },\n pullEvents: (request) => {\n return crdtStorage.getEventsBatch({\n afterSyncId: request.afterSyncId,\n status: \"applied\",\n excludeNodeId: request.excludeNodeId ?? \"\",\n limit: 100,\n });\n },\n goOnline: () => remoteSource.goOnline(),\n goOffline: () => remoteSource.goOffline(\"DISCONNECTED\"),\n dropConnection: () => remoteSource.dropConnection(\"NETWORK_OFFLINE\"),\n sync: () => {\n remoteSource.syncWithRemote().catch((error) => console.warn(\"Sync failed\", error));\n },\n requestReload: createReloadRequestHandler({\n resetState,\n broadcast: broadcastNotification,\n }),\n };\n\n broadcastChannels.requests.onmessage = (event) => {\n const message = event.data;\n\n if (!isWorkerRequestMessage(message)) {\n return;\n }\n\n const sendError = (error: unknown) => {\n const response: WorkerErrorResponseMessage = {\n type: \"error-response\",\n requestId: message.requestId,\n error: error instanceof Error ? error.message : String(error),\n };\n broadcastChannels.responses.postMessage(response);\n };\n\n try {\n const method = rpcTarget[message.method] as () => ReturnType<WorkerRpc[keyof WorkerRpc]>;\n const data = method.apply(null, message.args as []);\n\n if (data instanceof Promise) {\n data\n .then((result) => {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data: result,\n };\n broadcastChannels.responses.postMessage(response);\n })\n .catch(sendError);\n } else {\n const response: WorkerResponseMessage = {\n type: \"response\",\n requestId: message.requestId,\n data,\n };\n broadcastChannels.responses.postMessage(response);\n }\n } catch (error) {\n sendError(error);\n }\n };\n\n rpcTarget.postState();\n\n return async () => {\n stateChangedSubscription.unsubscribe();\n deSyncDetectedSubscription.unsubscribe();\n remoteSchemaVersionMismatchSubscription.unsubscribe();\n await remoteSource.dispose();\n broadcastChannels.requests.close();\n broadcastChannels.responses.close();\n db.close();\n };\n}\n\ntype InitRemoteOptions = {\n clientId: string;\n crdtStorage: CrdtStorage;\n migrator: SyncDbMigrator;\n pullSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n pushSyncId: ReturnType<KvStore[\"createNumberStoredValue\"]>;\n remoteFactory?: CreateRemoteSourceFactory;\n};\n\nfunction createRemoteSource({\n clientId,\n crdtStorage,\n migrator,\n pullSyncId,\n pushSyncId,\n remoteFactory,\n}: InitRemoteOptions) {\n return createCrdtSyncRemoteSource({\n bufferSize: PUSH_BATCH_SIZE,\n pullSyncId,\n pushSyncId,\n nodeId: clientId,\n storage: crdtStorage,\n migrator,\n remoteFactory,\n });\n}\n\nexport async function getWorkerConfig<Props = never>(): Promise<WorkerConfig<Props>> {\n let configSet = false;\n const responsePromise = createDeferredPromise<WorkerConfig>();\n\n self.onmessage = (event: MessageEvent<unknown>) => {\n if (configSet) {\n console.error(\"Worker config already set\");\n return;\n }\n\n const message = event.data;\n if (!isWorkerInitMessage(message)) {\n return;\n }\n\n responsePromise.resolve(message.config);\n configSet = true;\n };\n\n return responsePromise.promise;\n}\n\ntype WorkerOptions = {\n syncDbSchema: SyncDbSchema;\n logger?: Logger;\n createRemoteSource?: CreateRemoteSourceFactory;\n workerConfig?: WorkerConfig;\n /** Durable storage for reset state. Defaults to an IndexedDB-backed store. */\n resetStore?: ResetStore;\n /**\n * App-provided storage version, combined with the library's internal storage\n * version. Bump it when deploying a code change that old persisted local DBs\n * cannot survive — on mismatch the elected worker wipes the local DB on startup.\n */\n storageVersion?: string;\n /**\n * Dev-time drift check: replays the full migration history on a throwaway\n * in-memory database and verifies the result matches the schema declared with\n * `defineSyncSchema`. On mismatch the worker throws and refuses to start.\n * Enable in development only, e.g. `verifySchema: import.meta.env.DEV`.\n */\n verifySchema?: boolean;\n /**\n * Runs local worker event-log garbage collection on startup. Enabled by default;\n * set `eventLogGc: false` to disable. GC keeps the latest event rows and never removes\n * pending events or local rows that have not been pushed to the remote.\n * Skipped while an existing tab holds an in-memory replica of this DB.\n */\n eventLogGc?: boolean;\n};\n\nexport async function startDbWorker(opts: WorkerOptions) {\n const config = opts.workerConfig ?? (await getWorkerConfig());\n\n await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: \"exclusive\" }, async (lock) => {\n if (!lock) {\n return;\n }\n\n const cleanup = await createDbWorker(config, opts);\n\n const clientLockName = `${syncDbClientLockName}-${config.dbId}`;\n await new Promise<void>((resolve) => {\n const interval = setInterval(async () => {\n const { held } = await navigator.locks.query();\n const hasClients = held?.some((l) => l.name === clientLockName && l.mode === \"shared\");\n if (!hasClients) {\n clearInterval(interval);\n resolve();\n }\n }, 5_000);\n });\n\n await cleanup();\n });\n\n self.close();\n}\n\nfunction getMaxSyncId(db: SQLiteDbWrapper<WorkerDbSchema>, excludingStatus: \"none\" | \"pending\") {\n const [result] = db.executePrepared(\n \"get-max-sync-id\",\n { excludingStatus: excludingStatus as CrdtEventStatus },\n (db, params) =>\n db\n .selectFrom(\"worker.crdt_events\")\n .where(\"status\", \"!=\", params(\"excludingStatus\"))\n .select((eb) => eb.fn.max(\"sync_id\").as(\"sync_id\")),\n { loggerLevel: \"system\" },\n );\n\n return result?.sync_id ?? 0;\n}\n"],"mappings":"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"}
@@ -1,6 +1,31 @@
1
1
  import { l as generateId, o as createDeferredPromise, u as jsonSafeParse } from "./crdt-table-schema-D6Yf61Ex.js";
2
2
  import { t as HEARTBEAT_PING } from "./heartbeat-CJWQAoGP.js";
3
+ /** Rows deleted per statement, so each write transaction stays small. */
4
+ const EVENT_LOG_GC_CHUNK_SIZE = 1e3;
5
+ /** Rows deleted per startup at most; the rest is left for later startups. */
6
+ const EVENT_LOG_GC_MAX_DELETED_EVENTS = 5e3;
7
+ /**
8
+ * Deletes old applied/deduped events from the client event log. Runs on the startup path, so
9
+ * the work per run is bounded and a failure is logged instead of failing startup.
10
+ * Returns the number of deleted events.
11
+ */
3
12
  function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }) {
13
+ try {
14
+ const deleted = deleteOldEvents({
15
+ db,
16
+ dbConfig,
17
+ pushSyncId
18
+ });
19
+ logger?.("worker", `Event log GC deleted ${deleted} events`, "info");
20
+ return deleted;
21
+ } catch (error) {
22
+ const message = `Event log GC failed: ${error instanceof Error ? error.message : String(error)}`;
23
+ if (logger) logger("worker", message, "warning");
24
+ else console.warn(message);
25
+ return 0;
26
+ }
27
+ }
28
+ function deleteOldEvents({ db, dbConfig, pushSyncId }) {
4
29
  const eventsTable = dbConfig.eventsTable.fullIdentifier;
5
30
  const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
6
31
  key: "event-log-gc-delete-before-sync-id",
@@ -17,19 +42,40 @@ function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }) {
17
42
  params: [100],
18
43
  meta: { loggerLevel: "system" }
19
44
  });
20
- if (deleteBeforeSyncId === null) return;
21
- db.executePreparedRaw({
22
- key: "event-log-gc-delete-events",
23
- sql: `
24
- DELETE FROM ${eventsTable}
25
- WHERE "sync_id" < ?
26
- AND "status" IN ('applied', 'deduped')
27
- AND ("origin" = 'remote' OR "sync_id" <= ?)
28
- `,
29
- params: [deleteBeforeSyncId, pushSyncId.current],
30
- meta: { loggerLevel: "system" }
31
- });
32
- logger?.("worker", "Event log GC completed", "info");
45
+ if (deleteBeforeSyncId === null) return 0;
46
+ let deleted = 0;
47
+ while (deleted < EVENT_LOG_GC_MAX_DELETED_EVENTS) {
48
+ const limit = Math.min(EVENT_LOG_GC_CHUNK_SIZE, EVENT_LOG_GC_MAX_DELETED_EVENTS - deleted);
49
+ db.executePreparedRaw({
50
+ key: "event-log-gc-delete-events",
51
+ sql: `
52
+ DELETE FROM ${eventsTable}
53
+ WHERE "sync_id" IN (
54
+ SELECT "sync_id"
55
+ FROM ${eventsTable}
56
+ WHERE "sync_id" < ?
57
+ AND "status" IN ('applied', 'deduped')
58
+ AND ("origin" = 'remote' OR "sync_id" <= ?)
59
+ ORDER BY "sync_id" ASC
60
+ LIMIT ?
61
+ )
62
+ `,
63
+ params: [
64
+ deleteBeforeSyncId,
65
+ pushSyncId.current,
66
+ limit
67
+ ],
68
+ meta: { loggerLevel: "system" }
69
+ });
70
+ const [{ changes }] = db.executePreparedRaw({
71
+ key: "event-log-gc-changes",
72
+ sql: `SELECT changes() AS "changes"`,
73
+ meta: { loggerLevel: "system" }
74
+ });
75
+ deleted += changes;
76
+ if (changes < limit) break;
77
+ }
78
+ return deleted;
33
79
  }
34
80
  const storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
35
81
  function formatStorageVersion(appStorageVersion) {
@@ -158,4 +204,4 @@ const createWsRemoteSource = ({ createWebSocket }) => {
158
204
  //#endregion
159
205
  export { runWorkerEventLogGc as i, createStorageVersionStore as n, formatStorageVersion as r, createWsRemoteSource as t };
160
206
 
161
- //# sourceMappingURL=ws-remote-source-BbPWrHB6.js.map
207
+ //# sourceMappingURL=ws-remote-source-CliRqkIW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws-remote-source-BbPWrHB6.js","names":[],"sources":["../src/worker-db/event-log-gc.ts","../src/worker-db/storage-version.ts","../src/web-socket/ws-remote-source.ts"],"sourcesContent":["import type { Logger } from \"../logger\";\nimport type { SystemDbConfig } from \"../migrations/system-schema\";\nimport type { StoredValue } from \"../sqlite-crdt/stored-value\";\nimport type { InternalSQLiteWrapper } from \"../sqlite-db-wrapper\";\n\nexport const WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS = 100;\n\ntype EventLogGcOptions = {\n db: InternalSQLiteWrapper<any>;\n dbConfig: SystemDbConfig;\n pushSyncId: StoredValue<number>;\n logger?: Logger;\n};\n\nexport function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }: EventLogGcOptions) {\n const eventsTable = dbConfig.eventsTable.fullIdentifier;\n\n const [{ deleteBeforeSyncId }] = db.executePreparedRaw<[number], { deleteBeforeSyncId: number | null }>({\n key: \"event-log-gc-delete-before-sync-id\",\n sql: `\n SELECT min(\"sync_id\") AS deleteBeforeSyncId\n FROM (\n SELECT \"sync_id\"\n FROM ${eventsTable}\n WHERE \"status\" IN ('applied', 'deduped')\n ORDER BY \"sync_id\" DESC\n LIMIT ?\n )\n `,\n params: [WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS],\n meta: { loggerLevel: \"system\" },\n });\n\n if (deleteBeforeSyncId === null) {\n return;\n }\n\n db.executePreparedRaw<[number, number], never>({\n key: \"event-log-gc-delete-events\",\n sql: `\n DELETE FROM ${eventsTable}\n WHERE \"sync_id\" < ?\n AND \"status\" IN ('applied', 'deduped')\n AND (\"origin\" = 'remote' OR \"sync_id\" <= ?)\n `,\n params: [deleteBeforeSyncId, pushSyncId.current],\n meta: { loggerLevel: \"system\" },\n });\n\n logger?.(\"worker\", \"Event log GC completed\", \"info\");\n}\n","import type { ResetStore } from \"./reset-state\";\n\n/**\n * Internal persisted-storage format version. Bump when the library changes\n * the worker DB layout in a way old persisted databases cannot survive —\n * every client resets its local DB on the next worker start.\n */\nexport const LIB_STORAGE_VERSION = 2;\n\nconst storageVersionKey = (dbId: string) => `sqlite-sync-storage-version-${dbId}`;\n\nexport function formatStorageVersion(appStorageVersion: string | undefined): string {\n return appStorageVersion === undefined\n ? `lib-v${LIB_STORAGE_VERSION}`\n : `lib-v${LIB_STORAGE_VERSION}:app-${appStorageVersion}`;\n}\n\ntype StorageVersionStoreOptions = {\n store: ResetStore;\n dbId: string;\n /** Dev-provided app storage version, combined with the internal lib version. */\n appStorageVersion?: string;\n};\n\nexport type StorageVersionStore = ReturnType<typeof createStorageVersionStore>;\n\n/**\n * Worker-owned durable storage version. The current version combines the\n * internal lib version with the dev-provided app version; when the stored\n * version does not match (including a missing record), the elected worker\n * initializes with `clearOnInit: true`. Wiping on a missing record is\n * harmless for fresh installs and correctly resets databases persisted\n * before versioning existed.\n */\nexport function createStorageVersionStore({ store, dbId, appStorageVersion }: StorageVersionStoreOptions) {\n const key = storageVersionKey(dbId);\n const currentVersion = formatStorageVersion(appStorageVersion);\n\n return {\n currentVersion,\n async getVersionStatus(): Promise<\"current\" | \"missing\" | \"outdated\"> {\n const storedVersion = await store.get<string>(key);\n if (storedVersion === undefined) {\n return \"missing\";\n }\n return storedVersion === currentVersion ? \"current\" : \"outdated\";\n },\n /**\n * Record the current version. Must be called only after the worker has\n * successfully initialized with the wiped DB, so a failed init can be\n * retried by a later elected worker.\n */\n async markCurrentVersionApplied(): Promise<void> {\n await store.set(key, currentVersion);\n },\n };\n}\n","import type { SyncServerMessage, SyncServerRequest } from \"../server\";\nimport { HEARTBEAT_PING, HEARTBEAT_PONG } from \"../server/heartbeat\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport {\n createDeferredPromise,\n type DeferredPromise,\n type DistributiveOmit,\n generateId,\n jsonSafeParse,\n} from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\nconst WS_OPEN = 1;\n\ntype WsRemoteSourceConfig = {\n createWebSocket: () => Pick<WebSocket, \"onmessage\" | \"close\" | \"addEventListener\" | \"removeEventListener\"> & {\n readyState: number;\n send: (data: string) => void;\n };\n};\n\nexport const createWsRemoteSource = ({ createWebSocket }: WsRemoteSourceConfig): CreateRemoteSourceFactory => {\n return ({ onEventsAvailable, onClosed }) => {\n const socket = createWebSocket();\n const opened = createDeferredPromise<void>();\n opened.promise.catch(() => {});\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n let onPong: (() => void) | undefined;\n let closed = false;\n\n const shutdown = () => {\n if (closed) {\n return false;\n }\n closed = true;\n const error = new Error(\"WebSocket closed\");\n opened.reject(error);\n for (const request of requestsMap.values()) {\n request.reject(error);\n }\n requestsMap.clear();\n // Servers may not answer a close frame without a code, leaving the socket stuck in CLOSING.\n socket.close(1000);\n return true;\n };\n\n if (socket.readyState === WS_OPEN) {\n opened.resolve();\n }\n socket.addEventListener(\"open\", () => {\n opened.resolve();\n });\n // Closing on the first drop stops wrappers like PartySocket from reconnecting behind the library's back.\n socket.addEventListener(\"close\", () => {\n if (shutdown()) {\n onClosed();\n }\n });\n\n const send = async <T>(request: DistributiveOmit<SyncServerRequest, \"requestId\">): Promise<T> => {\n await opened.promise;\n if (closed) {\n throw new Error(\"WebSocket closed\");\n }\n const requestId = generateId();\n const promise = createDeferredPromise<T>();\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n socket.send(JSON.stringify({ ...request, requestId }));\n return promise.promise;\n };\n\n const pushEvents = (request: EventsPushRequest) =>\n send<EventsPushResponse>({ type: \"push-events\", nodeId: request.nodeId, events: request.events });\n\n const pullEvents = (request: EventsPullRequest) =>\n send<GetEventsBatch>({\n type: \"pull-events\",\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n });\n\n const ping = () => {\n socket.send(HEARTBEAT_PING);\n return new Promise<void>((resolve) => {\n onPong = resolve;\n });\n };\n\n socket.onmessage = (event) => {\n if (event.data === HEARTBEAT_PONG) {\n onPong?.();\n return;\n }\n\n const result = jsonSafeParse<SyncServerMessage>(event.data);\n\n if (!result.success || !(\"type\" in result.data) || !result.data.type) {\n return;\n }\n\n const message = result.data;\n\n switch (message.type) {\n case \"events-pull-response\":\n case \"events-push-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-applied\":\n onEventsAvailable({ newSyncId: message.newSyncId, remoteStateHash: message.stateHash ?? null });\n break;\n default:\n message satisfies never;\n return;\n }\n };\n\n return {\n pushEvents,\n pullEvents,\n ping,\n disconnect: () => {\n shutdown();\n },\n };\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-remote-source-CliRqkIW.js","names":[],"sources":["../src/worker-db/event-log-gc.ts","../src/worker-db/storage-version.ts","../src/web-socket/ws-remote-source.ts"],"sourcesContent":["import type { Logger } from \"../logger\";\nimport type { SystemDbConfig } from \"../migrations/system-schema\";\nimport type { StoredValue } from \"../sqlite-crdt/stored-value\";\nimport type { InternalSQLiteWrapper } from \"../sqlite-db-wrapper\";\n\nexport const WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS = 100;\n/** Rows deleted per statement, so each write transaction stays small. */\nexport const EVENT_LOG_GC_CHUNK_SIZE = 1000;\n/** Rows deleted per startup at most; the rest is left for later startups. */\nexport const EVENT_LOG_GC_MAX_DELETED_EVENTS = 5000;\n\ntype EventLogGcOptions = {\n db: InternalSQLiteWrapper<any>;\n dbConfig: SystemDbConfig;\n pushSyncId: StoredValue<number>;\n logger?: Logger;\n};\n\n/**\n * Deletes old applied/deduped events from the client event log. Runs on the startup path, so\n * the work per run is bounded and a failure is logged instead of failing startup.\n * Returns the number of deleted events.\n */\nexport function runWorkerEventLogGc({ db, dbConfig, pushSyncId, logger }: EventLogGcOptions): number {\n try {\n const deleted = deleteOldEvents({ db, dbConfig, pushSyncId });\n logger?.(\"worker\", `Event log GC deleted ${deleted} events`, \"info\");\n return deleted;\n } catch (error) {\n const message = `Event log GC failed: ${error instanceof Error ? error.message : String(error)}`;\n if (logger) {\n logger(\"worker\", message, \"warning\");\n } else {\n console.warn(message);\n }\n return 0;\n }\n}\n\nfunction deleteOldEvents({ db, dbConfig, pushSyncId }: Omit<EventLogGcOptions, \"logger\">): number {\n const eventsTable = dbConfig.eventsTable.fullIdentifier;\n\n const [{ deleteBeforeSyncId }] = db.executePreparedRaw<[number], { deleteBeforeSyncId: number | null }>({\n key: \"event-log-gc-delete-before-sync-id\",\n sql: `\n SELECT min(\"sync_id\") AS deleteBeforeSyncId\n FROM (\n SELECT \"sync_id\"\n FROM ${eventsTable}\n WHERE \"status\" IN ('applied', 'deduped')\n ORDER BY \"sync_id\" DESC\n LIMIT ?\n )\n `,\n params: [WORKER_EVENT_LOG_GC_MIN_RETAINED_EVENTS],\n meta: { loggerLevel: \"system\" },\n });\n\n if (deleteBeforeSyncId === null) {\n return 0;\n }\n\n let deleted = 0;\n while (deleted < EVENT_LOG_GC_MAX_DELETED_EVENTS) {\n const limit = Math.min(EVENT_LOG_GC_CHUNK_SIZE, EVENT_LOG_GC_MAX_DELETED_EVENTS - deleted);\n db.executePreparedRaw<[number, number, number], never>({\n key: \"event-log-gc-delete-events\",\n sql: `\n DELETE FROM ${eventsTable}\n WHERE \"sync_id\" IN (\n SELECT \"sync_id\"\n FROM ${eventsTable}\n WHERE \"sync_id\" < ?\n AND \"status\" IN ('applied', 'deduped')\n AND (\"origin\" = 'remote' OR \"sync_id\" <= ?)\n ORDER BY \"sync_id\" ASC\n LIMIT ?\n )\n `,\n params: [deleteBeforeSyncId, pushSyncId.current, limit],\n meta: { loggerLevel: \"system\" },\n });\n const [{ changes }] = db.executePreparedRaw<[], { changes: number }>({\n key: \"event-log-gc-changes\",\n sql: `SELECT changes() AS \"changes\"`,\n meta: { loggerLevel: \"system\" },\n });\n deleted += changes;\n if (changes < limit) {\n break;\n }\n }\n\n return deleted;\n}\n","import type { ResetStore } from \"./reset-state\";\n\n/**\n * Internal persisted-storage format version. Bump when the library changes\n * the worker DB layout in a way old persisted databases cannot survive —\n * every client resets its local DB on the next worker start.\n */\nexport const LIB_STORAGE_VERSION = 2;\n\nconst storageVersionKey = (dbId: string) => `sqlite-sync-storage-version-${dbId}`;\n\nexport function formatStorageVersion(appStorageVersion: string | undefined): string {\n return appStorageVersion === undefined\n ? `lib-v${LIB_STORAGE_VERSION}`\n : `lib-v${LIB_STORAGE_VERSION}:app-${appStorageVersion}`;\n}\n\ntype StorageVersionStoreOptions = {\n store: ResetStore;\n dbId: string;\n /** Dev-provided app storage version, combined with the internal lib version. */\n appStorageVersion?: string;\n};\n\nexport type StorageVersionStore = ReturnType<typeof createStorageVersionStore>;\n\n/**\n * Worker-owned durable storage version. The current version combines the\n * internal lib version with the dev-provided app version; when the stored\n * version does not match (including a missing record), the elected worker\n * initializes with `clearOnInit: true`. Wiping on a missing record is\n * harmless for fresh installs and correctly resets databases persisted\n * before versioning existed.\n */\nexport function createStorageVersionStore({ store, dbId, appStorageVersion }: StorageVersionStoreOptions) {\n const key = storageVersionKey(dbId);\n const currentVersion = formatStorageVersion(appStorageVersion);\n\n return {\n currentVersion,\n async getVersionStatus(): Promise<\"current\" | \"missing\" | \"outdated\"> {\n const storedVersion = await store.get<string>(key);\n if (storedVersion === undefined) {\n return \"missing\";\n }\n return storedVersion === currentVersion ? \"current\" : \"outdated\";\n },\n /**\n * Record the current version. Must be called only after the worker has\n * successfully initialized with the wiped DB, so a failed init can be\n * retried by a later elected worker.\n */\n async markCurrentVersionApplied(): Promise<void> {\n await store.set(key, currentVersion);\n },\n };\n}\n","import type { SyncServerMessage, SyncServerRequest } from \"../server\";\nimport { HEARTBEAT_PING, HEARTBEAT_PONG } from \"../server/heartbeat\";\nimport type { CreateRemoteSourceFactory } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport {\n createDeferredPromise,\n type DeferredPromise,\n type DistributiveOmit,\n generateId,\n jsonSafeParse,\n} from \"../utils\";\nimport type { EventsPullRequest, EventsPushRequest, EventsPushResponse, GetEventsBatch } from \"../worker\";\n\nconst WS_OPEN = 1;\n\ntype WsRemoteSourceConfig = {\n createWebSocket: () => Pick<WebSocket, \"onmessage\" | \"close\" | \"addEventListener\" | \"removeEventListener\"> & {\n readyState: number;\n send: (data: string) => void;\n };\n};\n\nexport const createWsRemoteSource = ({ createWebSocket }: WsRemoteSourceConfig): CreateRemoteSourceFactory => {\n return ({ onEventsAvailable, onClosed }) => {\n const socket = createWebSocket();\n const opened = createDeferredPromise<void>();\n opened.promise.catch(() => {});\n const requestsMap = new Map<string, DeferredPromise<unknown>>();\n let onPong: (() => void) | undefined;\n let closed = false;\n\n const shutdown = () => {\n if (closed) {\n return false;\n }\n closed = true;\n const error = new Error(\"WebSocket closed\");\n opened.reject(error);\n for (const request of requestsMap.values()) {\n request.reject(error);\n }\n requestsMap.clear();\n // Servers may not answer a close frame without a code, leaving the socket stuck in CLOSING.\n socket.close(1000);\n return true;\n };\n\n if (socket.readyState === WS_OPEN) {\n opened.resolve();\n }\n socket.addEventListener(\"open\", () => {\n opened.resolve();\n });\n // Closing on the first drop stops wrappers like PartySocket from reconnecting behind the library's back.\n socket.addEventListener(\"close\", () => {\n if (shutdown()) {\n onClosed();\n }\n });\n\n const send = async <T>(request: DistributiveOmit<SyncServerRequest, \"requestId\">): Promise<T> => {\n await opened.promise;\n if (closed) {\n throw new Error(\"WebSocket closed\");\n }\n const requestId = generateId();\n const promise = createDeferredPromise<T>();\n requestsMap.set(requestId, promise as DeferredPromise<unknown>);\n socket.send(JSON.stringify({ ...request, requestId }));\n return promise.promise;\n };\n\n const pushEvents = (request: EventsPushRequest) =>\n send<EventsPushResponse>({ type: \"push-events\", nodeId: request.nodeId, events: request.events });\n\n const pullEvents = (request: EventsPullRequest) =>\n send<GetEventsBatch>({\n type: \"pull-events\",\n afterSyncId: request.afterSyncId,\n excludeNodeId: request.excludeNodeId,\n });\n\n const ping = () => {\n socket.send(HEARTBEAT_PING);\n return new Promise<void>((resolve) => {\n onPong = resolve;\n });\n };\n\n socket.onmessage = (event) => {\n if (event.data === HEARTBEAT_PONG) {\n onPong?.();\n return;\n }\n\n const result = jsonSafeParse<SyncServerMessage>(event.data);\n\n if (!result.success || !(\"type\" in result.data) || !result.data.type) {\n return;\n }\n\n const message = result.data;\n\n switch (message.type) {\n case \"events-pull-response\":\n case \"events-push-response\": {\n const promise = requestsMap.get(message.requestId);\n if (!promise) {\n return;\n }\n promise.resolve(message.data);\n requestsMap.delete(message.requestId);\n break;\n }\n case \"events-applied\":\n onEventsAvailable({ newSyncId: message.newSyncId, remoteStateHash: message.stateHash ?? null });\n break;\n default:\n message satisfies never;\n return;\n }\n };\n\n return {\n pushEvents,\n pullEvents,\n ping,\n disconnect: () => {\n shutdown();\n },\n };\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlite-sync/core",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "SQLite synchronization library with CRDT support",
5
5
  "type": "module",
6
6
  "license": "MIT",