@sqlite-sync/core 0.12.3 → 0.13.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.
Files changed (44) hide show
  1. package/dist/{reset-state-t0tTGqoh.js → crdt-sync-producer-BY-WobgO.js} +55 -241
  2. package/dist/crdt-sync-producer-BY-WobgO.js.map +1 -0
  3. package/dist/{crdt-sync-remote-source-CPXzmSIS.js → crdt-sync-remote-source-yeTEkOjH.js} +344 -12
  4. package/dist/crdt-sync-remote-source-yeTEkOjH.js.map +1 -0
  5. package/dist/{crdt-table-schema-BRm_WdcW.js → crdt-table-schema-D6Yf61Ex.js} +37 -13
  6. package/dist/crdt-table-schema-D6Yf61Ex.js.map +1 -0
  7. package/dist/{index-BHsKLqcK.d.ts → index-CH4lf3iX.d.ts} +166 -72
  8. package/dist/index.common-C8W9VKSY.js +276 -0
  9. package/dist/index.common-C8W9VKSY.js.map +1 -0
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +20 -677
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.native.d.ts +2 -0
  14. package/dist/index.native.js +9 -0
  15. package/dist/internal.d.ts +1 -1
  16. package/dist/internal.js +2 -2
  17. package/dist/local.d.ts +48 -0
  18. package/dist/local.js +255 -0
  19. package/dist/local.js.map +1 -0
  20. package/dist/{make-crdt-table-C6l0F-o_.js → make-crdt-table-DYCXOYB7.js} +13 -14
  21. package/dist/make-crdt-table-DYCXOYB7.js.map +1 -0
  22. package/dist/reset-state-CPOi_45B.js +220 -0
  23. package/dist/reset-state-CPOi_45B.js.map +1 -0
  24. package/dist/server.d.ts +8 -3
  25. package/dist/server.js +2 -2
  26. package/dist/server.js.map +1 -1
  27. package/dist/sqlite-reactive-db-DLS1xRZp.js +296 -0
  28. package/dist/sqlite-reactive-db-DLS1xRZp.js.map +1 -0
  29. package/dist/synced-db-database-B5M7MrBD.js +81 -0
  30. package/dist/synced-db-database-B5M7MrBD.js.map +1 -0
  31. package/dist/{system-schema-Dvl_BLId.js → system-schema-CXf3FmxN.js} +142 -58
  32. package/dist/system-schema-CXf3FmxN.js.map +1 -0
  33. package/dist/worker.d.ts +2 -10
  34. package/dist/worker.js +11 -177
  35. package/dist/worker.js.map +1 -1
  36. package/dist/ws-remote-source-BbPWrHB6.js +161 -0
  37. package/dist/ws-remote-source-BbPWrHB6.js.map +1 -0
  38. package/dist/ws-remote-source-DFBhDc6Z.d.ts +12 -0
  39. package/package.json +9 -1
  40. package/dist/crdt-sync-remote-source-CPXzmSIS.js.map +0 -1
  41. package/dist/crdt-table-schema-BRm_WdcW.js.map +0 -1
  42. package/dist/make-crdt-table-C6l0F-o_.js.map +0 -1
  43. package/dist/reset-state-t0tTGqoh.js.map +0 -1
  44. package/dist/system-schema-Dvl_BLId.js.map +0 -1
@@ -0,0 +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: getMaxSyncId(db),\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 remoteSource.addEventListener(\"state-changed\", () => {\n notify({\n notificationType: \"state-changed\",\n state: remoteSource.getState(),\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: () => remoteSource.getState(),\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,7 +1,7 @@
1
- import { p as quoteId } from "./crdt-table-schema-BRm_WdcW.js";
1
+ import { p as quoteId } from "./crdt-table-schema-D6Yf61Ex.js";
2
2
  //#region src/sqlite-crdt/make-crdt-table.ts
3
3
  const CRDT_CHANGE_INTENTS_TABLE = "crdt_change_intents";
4
- function makeCrdtTable({ db, baseTableName, crdtTableName }) {
4
+ function makeCrdtTable({ db, baseTableName, crdtTableName, temp }) {
5
5
  const tableSchema = db.dbSchema[baseTableName];
6
6
  if (!tableSchema) throw new Error(`Table ${baseTableName} not found`);
7
7
  const columns = new Map(tableSchema.columns.map((column) => [column.name, column]));
@@ -18,7 +18,8 @@ function makeCrdtTable({ db, baseTableName, crdtTableName }) {
18
18
  columns: tableSchema.columns.map((column) => ({
19
19
  name: column.name,
20
20
  nullable: column.isNullable
21
- }))
21
+ })),
22
+ temp
22
23
  })) db.execute(sql, { loggerLevel: "system" });
23
24
  }
24
25
  function concatSql(parts) {
@@ -57,11 +58,12 @@ function sparseUpdatePayloadSql(columnNames) {
57
58
  "'}'"
58
59
  ]);
59
60
  }
60
- function createCrdtViewStatements({ baseTableName, crdtTableName, columns }) {
61
+ function createCrdtViewStatements({ baseTableName, crdtTableName, columns, temp = false }) {
61
62
  const createdPayload = createdPayloadSql(columns);
62
63
  const updatePayload = sparseUpdatePayloadSql(columns.map((column) => column.name));
64
+ const tempKeyword = temp ? "temp " : "";
63
65
  return [
64
- `create table if not exists ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (
66
+ `create ${tempKeyword}table if not exists ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (
65
67
  "seq" integer primary key,
66
68
  "dataset" text not null,
67
69
  "type" text not null,
@@ -69,10 +71,10 @@ function createCrdtViewStatements({ baseTableName, crdtTableName, columns }) {
69
71
  "new_item_id" text,
70
72
  "payload_json" text not null
71
73
  )`,
72
- `create view ${quoteId(crdtTableName)} as
74
+ `create ${tempKeyword}view ${quoteId(crdtTableName)} as
73
75
  select * from ${quoteId(baseTableName)}
74
76
  where tombstone = 0`,
75
- `create trigger ${quoteId(`${crdtTableName}_created`)}
77
+ `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_created`)}
76
78
  instead of insert on ${quoteId(crdtTableName)}
77
79
  for each row
78
80
  begin
@@ -82,7 +84,7 @@ begin
82
84
  ${sqlStringLiteral(baseTableName)}, 'item-created', new."id", null, ${createdPayload}
83
85
  );
84
86
  end`,
85
- `create trigger ${quoteId(`${crdtTableName}_updated`)}
87
+ `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_updated`)}
86
88
  instead of update on ${quoteId(crdtTableName)}
87
89
  for each row
88
90
  begin
@@ -92,7 +94,7 @@ begin
92
94
  ${sqlStringLiteral(baseTableName)}, 'item-updated', old."id", new."id", ${updatePayload}
93
95
  );
94
96
  end`,
95
- `create trigger ${quoteId(`${crdtTableName}_deleted`)}
97
+ `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_deleted`)}
96
98
  instead of delete on ${quoteId(crdtTableName)}
97
99
  for each row
98
100
  when old."tombstone" = 0
@@ -109,7 +111,7 @@ function registerCrdtIntentDrainer({ reactiveDb, storage }) {
109
111
  let eventApplied = false;
110
112
  let processedIntentMutationVersion = reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE);
111
113
  reactiveDb.db.setAfterMutatingStatement((tx) => {
112
- if (reactiveDb.getTableMutationVersion("crdt_change_intents") === processedIntentMutationVersion) return;
114
+ if (reactiveDb.hasSynchronousHooks && reactiveDb.getTableMutationVersion("crdt_change_intents") === processedIntentMutationVersion) return;
113
115
  try {
114
116
  if (drainCrdtChangeIntents({
115
117
  tx,
@@ -125,9 +127,6 @@ function registerCrdtIntentDrainer({ reactiveDb, storage }) {
125
127
  storage.internal.processEnqueuedEvents();
126
128
  }
127
129
  });
128
- reactiveDb.addEventListener("transaction-rolled-back", () => {
129
- eventApplied = false;
130
- });
131
130
  }
132
131
  function drainCrdtChangeIntents({ tx, storage }) {
133
132
  const intents = tx.executePreparedRaw({
@@ -142,4 +141,4 @@ function drainCrdtChangeIntents({ tx, storage }) {
142
141
  //#endregion
143
142
  export { makeCrdtTable as a, drainCrdtChangeIntents as i, crdtViewColumnsFromTable as n, registerCrdtIntentDrainer as o, createCrdtViewStatements as r, CRDT_CHANGE_INTENTS_TABLE as t };
144
143
 
145
- //# sourceMappingURL=make-crdt-table-C6l0F-o_.js.map
144
+ //# sourceMappingURL=make-crdt-table-DYCXOYB7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"make-crdt-table-DYCXOYB7.js","names":[],"sources":["../src/sqlite-crdt/make-crdt-table.ts"],"sourcesContent":["import type { SQLiteReactiveDb } from \"../memory-db/sqlite-reactive-db\";\nimport type { AnyTableBuilder } from \"../schema/table-builder\";\nimport type { InternalSQLiteTransactionWrapper, SQLiteDbWrapper } from \"../sqlite-db-wrapper\";\nimport { quoteId } from \"../utils\";\nimport type { CrdtChangeIntent, InternalCrdtStorage } from \"./crdt-storage\";\n\nexport const CRDT_CHANGE_INTENTS_TABLE = \"crdt_change_intents\";\n\nexport function makeCrdtTable({\n db,\n baseTableName,\n crdtTableName,\n temp,\n}: {\n db: SQLiteDbWrapper<any>;\n baseTableName: string;\n crdtTableName: string;\n temp?: boolean;\n}) {\n const tableSchema = db.dbSchema[baseTableName];\n\n if (!tableSchema) {\n throw new Error(`Table ${baseTableName} not found`);\n }\n\n const columns = new Map(tableSchema.columns.map((column) => [column.name, column]));\n\n const idColumn = columns.get(\"id\");\n if (!idColumn) {\n throw new Error(\n `Table \"${baseTableName}\" is missing a required \"id\" column. CRDT tables must have an \"id\" column to identify items.`,\n );\n }\n if (idColumn.dataType.toUpperCase() !== \"TEXT\") {\n throw new Error(\n `Table \"${baseTableName}\": \"id\" column must be of type TEXT, got \"${idColumn.dataType}\". CRDT item IDs are stored as strings.`,\n );\n }\n\n const tombstoneColumn = columns.get(\"tombstone\");\n if (!tombstoneColumn) {\n throw new Error(\n `Table \"${baseTableName}\" is missing a required \"tombstone\" column. CRDT tables must have a \"tombstone\" INTEGER column for soft deletes.`,\n );\n }\n const tombstoneType = tombstoneColumn.dataType.toUpperCase();\n if (tombstoneType !== \"INTEGER\" && tombstoneType !== \"BOOLEAN\") {\n throw new Error(\n `Table \"${baseTableName}\": \"tombstone\" column must be of type INTEGER or BOOLEAN, got \"${tombstoneColumn.dataType}\". It is compared as 0/1 for soft deletes.`,\n );\n }\n\n for (const sql of createCrdtViewStatements({\n baseTableName,\n crdtTableName,\n columns: tableSchema.columns.map((column) => ({ name: column.name, nullable: column.isNullable })),\n temp,\n })) {\n db.execute(sql, { loggerLevel: \"system\" });\n }\n}\n\n// Workerd caps expr depth at 100; a left-deep `||` chain fails around 34 columns.\nfunction concatSql(parts: readonly string[]): string {\n if (parts.length === 0) {\n return \"''\";\n }\n if (parts.length === 1) {\n return parts[0];\n }\n const mid = parts.length >> 1;\n return `(${concatSql(parts.slice(0, mid))})||(${concatSql(parts.slice(mid))})`;\n}\n\nfunction sqlStringLiteral(value: string) {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nexport type CrdtViewColumn = {\n name: string;\n nullable: boolean;\n};\n\nexport function crdtViewColumnsFromTable(table: AnyTableBuilder): CrdtViewColumn[] {\n return Object.entries(table.columns).map(([name, meta]) => ({ name, nullable: meta.nullable }));\n}\n\n// Columns omitted from an insert are NULL in an INSTEAD OF trigger. NULLs of NOT NULL columns are\n// left out of the payload so applying it falls back to the table DEFAULT on every replica.\n// The tombstone is left out: applying a create always sets it to 0.\nfunction createdPayloadSql(columns: CrdtViewColumn[]) {\n const fields = columns\n .filter((column) => column.name !== \"tombstone\")\n .map((column) => {\n const quoted = quoteId(column.name);\n const field = `${sqlStringLiteral(`${JSON.stringify(column.name)}:`)}||json_quote(new.${quoted})||','`;\n return column.nullable ? field : `case when new.${quoted} is null then '' else ${field} end`;\n });\n return concatSql([\"'{'\", `rtrim(${concatSql(fields)}, ',')`, \"'}'\"]);\n}\n\nfunction sparseUpdatePayloadSql(columnNames: string[]) {\n const changedFields = columnNames\n .filter((columnName) => columnName !== \"id\")\n .map((columnName) => {\n const quoted = quoteId(columnName);\n const key = sqlStringLiteral(`${JSON.stringify(columnName)}:`);\n return `case when old.${quoted} collate binary is not new.${quoted} then ${key}||json_quote(new.${quoted})||',' else '' end`;\n });\n return concatSql([\"'{'\", `rtrim(${concatSql(changedFields)}, ',')`, \"'}'\"]);\n}\n\nexport function createCrdtViewStatements({\n baseTableName,\n crdtTableName,\n columns,\n temp = false,\n}: {\n baseTableName: string;\n crdtTableName: string;\n columns: CrdtViewColumn[];\n /**\n * Create the view, its triggers and the intents table in the connection's temp schema,\n * so a persisted database never stores them and they always match the current schema.\n */\n temp?: boolean;\n}) {\n const createdPayload = createdPayloadSql(columns);\n const updatePayload = sparseUpdatePayloadSql(columns.map((column) => column.name));\n const tempKeyword = temp ? \"temp \" : \"\";\n\n return [\n `create ${tempKeyword}table if not exists ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"seq\" integer primary key,\n \"dataset\" text not null,\n \"type\" text not null,\n \"item_id\" text not null,\n \"new_item_id\" text,\n \"payload_json\" text not null\n)`,\n `create ${tempKeyword}view ${quoteId(crdtTableName)} as\nselect * from ${quoteId(baseTableName)}\nwhere tombstone = 0`,\n `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_created`)}\ninstead of insert on ${quoteId(crdtTableName)}\nfor each row\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-created', new.\"id\", null, ${createdPayload}\n );\nend`,\n `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_updated`)}\ninstead of update on ${quoteId(crdtTableName)}\nfor each row\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-updated', old.\"id\", new.\"id\", ${updatePayload}\n );\nend`,\n `create ${tempKeyword}trigger ${quoteId(`${crdtTableName}_deleted`)}\ninstead of delete on ${quoteId(crdtTableName)}\nfor each row\nwhen old.\"tombstone\" = 0\nbegin\n insert into ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} (\n \"dataset\", \"type\", \"item_id\", \"new_item_id\", \"payload_json\"\n ) values (\n ${sqlStringLiteral(baseTableName)}, 'item-deleted', old.\"id\", null, '{}'\n );\nend`,\n ];\n}\n\nexport function registerCrdtIntentDrainer({\n reactiveDb,\n storage,\n}: {\n reactiveDb: SQLiteReactiveDb<any>;\n storage: InternalCrdtStorage;\n}) {\n let eventApplied = false;\n let processedIntentMutationVersion = reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE);\n\n reactiveDb.db.setAfterMutatingStatement((tx) => {\n if (\n reactiveDb.hasSynchronousHooks &&\n reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE) === processedIntentMutationVersion\n ) {\n return;\n }\n\n try {\n if (drainCrdtChangeIntents({ tx, storage })) {\n eventApplied = true;\n }\n } finally {\n processedIntentMutationVersion = reactiveDb.getTableMutationVersion(CRDT_CHANGE_INTENTS_TABLE);\n }\n });\n\n // A rolled-back drain leaves `eventApplied` set on purpose: with asynchronous hooks the\n // rollback can be delivered after a later transaction drained again, and clearing the flag\n // then would strand that transaction's events. Processing with nothing pending is a no-op.\n reactiveDb.addEventListener(\"transaction-committed\", () => {\n if (eventApplied) {\n eventApplied = false;\n void storage.internal.processEnqueuedEvents();\n }\n });\n}\n\nexport function drainCrdtChangeIntents({\n tx,\n storage,\n}: {\n tx: InternalSQLiteTransactionWrapper<any>;\n storage: InternalCrdtStorage;\n}) {\n const intents = tx.executePreparedRaw<[], CrdtChangeIntent>({\n key: \"drain-crdt-change-intents\",\n sql: `delete from ${quoteId(CRDT_CHANGE_INTENTS_TABLE)} returning *`,\n meta: { loggerLevel: \"system\" },\n });\n intents.sort((a, b) => a.seq - b.seq);\n\n const { appliedEvents } = storage.internal.applyOwnIntentsFromTransaction(tx, intents);\n return appliedEvents > 0;\n}\n"],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { a as acquireLock, l as generateId, r as TypedBroadcastChannel } from "./crdt-table-schema-D6Yf61Ex.js";
2
+ import { o as verifySyncSchemaWithDriver } from "./crdt-sync-producer-BY-WobgO.js";
3
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
4
+ import { createStore, del, get, set } from "idb-keyval";
5
+ //#region src/wasm-driver.ts
6
+ let sqliteModulePromise = null;
7
+ function loadSqliteWasm() {
8
+ sqliteModulePromise ??= sqlite3InitModule();
9
+ return sqliteModulePromise;
10
+ }
11
+ async function createWasmMemoryDriver() {
12
+ const sqlite3 = await loadSqliteWasm();
13
+ return createWasmSqliteDriver(sqlite3, new sqlite3.oo1.DB({ filename: ":memory:" }));
14
+ }
15
+ function createWasmSqliteDriver(sqlite3, db) {
16
+ const { capi } = sqlite3;
17
+ const prepare = (sql) => {
18
+ const statement = db.prepare(sql);
19
+ const pointer = statement.pointer;
20
+ if (!pointer) throw new Error("Prepared statement is already finalized");
21
+ return {
22
+ readonly: capi.sqlite3_stmt_readonly(pointer) !== 0,
23
+ run: (parameters) => {
24
+ try {
25
+ if (parameters.length > 0) statement.bind(parameters);
26
+ const rows = [];
27
+ let columns;
28
+ while (statement.step()) {
29
+ columns ??= statement.getColumnNames([]);
30
+ rows.push(statement.get([]));
31
+ }
32
+ return {
33
+ columns: columns ?? [],
34
+ rows
35
+ };
36
+ } finally {
37
+ statement.reset(true);
38
+ }
39
+ },
40
+ finalize: () => {
41
+ statement.finalize();
42
+ }
43
+ };
44
+ };
45
+ return {
46
+ prepare,
47
+ synchronousHooks: true,
48
+ totalChanges: () => capi.sqlite3_total_changes(db),
49
+ setHooks: (hooks) => {
50
+ if (!hooks) {
51
+ capi.sqlite3_update_hook(db, 0, 0);
52
+ capi.sqlite3_rollback_hook(db, 0, 0);
53
+ capi.sqlite3_commit_hook(db, 0, 0);
54
+ return;
55
+ }
56
+ capi.sqlite3_update_hook(db, (_ctx, _opId, _db, table) => hooks.onUpdate(table), 0);
57
+ capi.sqlite3_rollback_hook(db, () => {
58
+ hooks.onRollback();
59
+ return 0;
60
+ }, 0);
61
+ capi.sqlite3_commit_hook(db, () => {
62
+ hooks.onCommit();
63
+ return 0;
64
+ }, 0);
65
+ },
66
+ serialize: () => capi.sqlite3_js_db_export(db),
67
+ deserialize: (snapshot) => {
68
+ const dataPointer = sqlite3.wasm.allocFromTypedArray(snapshot);
69
+ const resultCode = capi.sqlite3_deserialize(db, "main", dataPointer, snapshot.byteLength, snapshot.byteLength, capi.SQLITE_DESERIALIZE_FREEONCLOSE | capi.SQLITE_DESERIALIZE_RESIZEABLE);
70
+ db.checkRc(resultCode);
71
+ },
72
+ createScalarFunction: ({ name, callback, arity, deterministic, directOnly, innocuous }) => {
73
+ db.createFunction({
74
+ name,
75
+ xFunc: (_, ...args) => callback(...args),
76
+ arity,
77
+ deterministic,
78
+ directOnly,
79
+ innocuous
80
+ });
81
+ },
82
+ close: () => {
83
+ db.close();
84
+ }
85
+ };
86
+ }
87
+ //#endregion
88
+ //#region src/schema/verify-sync-schema-wasm.ts
89
+ /** {@link verifySyncSchemaWithDriver} on a throwaway in-memory sqlite-wasm database. */
90
+ async function verifySyncSchema(schema) {
91
+ return verifySyncSchemaWithDriver(schema, await createWasmMemoryDriver());
92
+ }
93
+ //#endregion
94
+ //#region src/worker-db/worker-common.ts
95
+ const syncDbWorkerLockName = "sync-db-worker-lock";
96
+ const syncDbClientLockName = "sync-db-client-lock";
97
+ const broadcastChannelNames = {
98
+ requests: "sync-db-worker-requests",
99
+ responses: "sync-db-worker-responses"
100
+ };
101
+ const createBroadcastChannels = (prefix) => {
102
+ return {
103
+ requests: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.requests}`),
104
+ responses: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.responses}`)
105
+ };
106
+ };
107
+ function isWorkerInitMessage(message) {
108
+ return typeof message === "object" && message !== null && "type" in message && message.type === "init";
109
+ }
110
+ function isWorkerRequestMessage(message) {
111
+ return typeof message === "object" && message !== null && "type" in message && message.type === "request";
112
+ }
113
+ function isWorkerResponseMessage(message) {
114
+ return typeof message === "object" && message !== null && "type" in message && "requestId" in message && "data" in message;
115
+ }
116
+ function isWorkerErrorResponseMessage(message) {
117
+ return typeof message === "object" && message !== null && "type" in message && message.type === "error-response";
118
+ }
119
+ function isWorkerNotificationMessage(message) {
120
+ return typeof message === "object" && message !== null && "notificationType" in message && !!message.notificationType;
121
+ }
122
+ //#endregion
123
+ //#region src/worker-db/replica-lock.ts
124
+ const replicaLockName = (dbId) => `sync-db-replica-lock-${dbId}`;
125
+ function acquireReplicaLock({ dbId }) {
126
+ return acquireLock(replicaLockName(dbId), { mode: "shared" });
127
+ }
128
+ function runWithNoActiveReplicas({ dbId, run }) {
129
+ return navigator.locks.request(replicaLockName(dbId), {
130
+ mode: "exclusive",
131
+ ifAvailable: true
132
+ }, async (lock) => {
133
+ if (!lock) return false;
134
+ await run();
135
+ return true;
136
+ });
137
+ }
138
+ //#endregion
139
+ //#region src/worker-db/reset-state.ts
140
+ /** Default IndexedDB-backed reset store (idb-keyval over a dedicated database). */
141
+ function createIdbResetStore() {
142
+ const store = createStore("sqlite-sync", "kv");
143
+ return {
144
+ get: (key) => get(key, store),
145
+ set: (key, value) => set(key, value, store),
146
+ delete: (key) => del(key, store)
147
+ };
148
+ }
149
+ /**
150
+ * A clean reset is a recovery action for a de-sync detected now. If the reload
151
+ * never happens (broadcast lost, tab crashed mid-reload, browser killed the page),
152
+ * the request must not fire on an arbitrary later cold start and silently wipe
153
+ * local-only writes accumulated since.
154
+ */
155
+ const RESET_REQUEST_TTL_MS = 6e5;
156
+ const resetRequestKey = (dbId) => `sqlite-sync-reset-request-${dbId}`;
157
+ const resetAppliedKey = (dbId) => `sqlite-sync-reset-applied-${dbId}`;
158
+ /**
159
+ * Worker-owned durable reset state. The reset decision must be owned by the
160
+ * elected worker, not by tabs during `createSyncedDb` — otherwise a later
161
+ * worker election could repeat an already-applied wipe.
162
+ */
163
+ function createResetStateStore({ store, dbId, now = () => Date.now() }) {
164
+ const requestKey = resetRequestKey(dbId);
165
+ const appliedKey = resetAppliedKey(dbId);
166
+ return {
167
+ async writeResetRequest(epoch) {
168
+ const request = {
169
+ epoch,
170
+ requestedAt: now()
171
+ };
172
+ await store.set(requestKey, request);
173
+ return request;
174
+ },
175
+ /**
176
+ * Read the pending reset request after winning the worker election.
177
+ * Returns the request only when it has not been applied yet and is within
178
+ * the TTL. Stale requests are deleted so they cannot fire on a later cold start.
179
+ */
180
+ async resolvePendingReset() {
181
+ const request = await store.get(requestKey);
182
+ if (!request) return;
183
+ if (now() - request.requestedAt > 6e5) {
184
+ await store.delete(requestKey);
185
+ return;
186
+ }
187
+ const appliedEpoch = await store.get(appliedKey);
188
+ if (request.epoch === appliedEpoch) return;
189
+ return request;
190
+ },
191
+ /**
192
+ * Record the epoch as applied. Must be called only after the worker has
193
+ * successfully initialized with `clearOnInit: true`, so a failed init can
194
+ * be retried by a later elected worker.
195
+ */
196
+ async markResetApplied(epoch) {
197
+ await store.set(appliedKey, epoch);
198
+ }
199
+ };
200
+ }
201
+ /**
202
+ * Worker-side `requestReload` RPC implementation. For `clean: true` the reset
203
+ * request is durably stored before broadcasting and before the RPC resolves,
204
+ * so the epoch survives no matter which path triggers the reload.
205
+ */
206
+ function createReloadRequestHandler({ resetState, broadcast, generateEpoch = generateId }) {
207
+ return async (options) => {
208
+ const reloadEpoch = generateEpoch();
209
+ if (options.clean) await resetState.writeResetRequest(reloadEpoch);
210
+ broadcast({
211
+ notificationType: "reload-requested",
212
+ reloadEpoch,
213
+ clean: options.clean
214
+ });
215
+ };
216
+ }
217
+ //#endregion
218
+ export { createWasmSqliteDriver as _, acquireReplicaLock as a, isWorkerErrorResponseMessage as c, isWorkerRequestMessage as d, isWorkerResponseMessage as f, createWasmMemoryDriver as g, verifySyncSchema as h, createResetStateStore as i, isWorkerInitMessage as l, syncDbWorkerLockName as m, createIdbResetStore as n, runWithNoActiveReplicas as o, syncDbClientLockName as p, createReloadRequestHandler as r, createBroadcastChannels as s, RESET_REQUEST_TTL_MS as t, isWorkerNotificationMessage as u, loadSqliteWasm as v };
219
+
220
+ //# sourceMappingURL=reset-state-CPOi_45B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-state-CPOi_45B.js","names":[],"sources":["../src/wasm-driver.ts","../src/schema/verify-sync-schema-wasm.ts","../src/worker-db/worker-common.ts","../src/worker-db/replica-lock.ts","../src/worker-db/reset-state.ts"],"sourcesContent":["import sqlite3InitModule, {\n type BindableValue,\n type Database,\n type Sqlite3Static,\n type SqlValue,\n} from \"@sqlite.org/sqlite-wasm\";\nimport type { SqliteDriver, SqliteDriverStatement, SqliteDriverValue } from \"./sqlite-driver\";\n\nlet sqliteModulePromise: Promise<Sqlite3Static> | null = null;\n\nexport function loadSqliteWasm() {\n sqliteModulePromise ??= sqlite3InitModule();\n return sqliteModulePromise;\n}\n\nexport async function createWasmMemoryDriver() {\n const sqlite3 = await loadSqliteWasm();\n return createWasmSqliteDriver(sqlite3, new sqlite3.oo1.DB({ filename: \":memory:\" }));\n}\n\nexport function createWasmSqliteDriver(sqlite3: Sqlite3Static, db: Database): SqliteDriver {\n const { capi } = sqlite3;\n\n const prepare = (sql: string): SqliteDriverStatement => {\n const statement = db.prepare(sql);\n const pointer = statement.pointer;\n if (!pointer) {\n throw new Error(\"Prepared statement is already finalized\");\n }\n\n return {\n readonly: capi.sqlite3_stmt_readonly(pointer) !== 0,\n run: (parameters) => {\n try {\n if (parameters.length > 0) {\n statement.bind(parameters as BindableValue[]);\n }\n\n const rows: SqliteDriverValue[][] = [];\n let columns: string[] | undefined;\n while (statement.step()) {\n columns ??= statement.getColumnNames([]);\n rows.push(statement.get([]) as SqliteDriverValue[]);\n }\n return { columns: columns ?? [], rows };\n } finally {\n statement.reset(true);\n }\n },\n finalize: () => {\n statement.finalize();\n },\n };\n };\n\n return {\n prepare,\n synchronousHooks: true,\n totalChanges: () => capi.sqlite3_total_changes(db),\n setHooks: (hooks) => {\n if (!hooks) {\n capi.sqlite3_update_hook(db, 0 as never, 0);\n capi.sqlite3_rollback_hook(db, 0, 0);\n capi.sqlite3_commit_hook(db, 0, 0);\n return;\n }\n\n capi.sqlite3_update_hook(db, (_ctx, _opId, _db, table) => hooks.onUpdate(table), 0);\n capi.sqlite3_rollback_hook(\n db,\n () => {\n hooks.onRollback();\n return 0;\n },\n 0,\n );\n capi.sqlite3_commit_hook(\n db,\n () => {\n hooks.onCommit();\n return 0;\n },\n 0,\n );\n },\n serialize: () => capi.sqlite3_js_db_export(db),\n deserialize: (snapshot) => {\n const dataPointer = sqlite3.wasm.allocFromTypedArray(snapshot);\n const resultCode = capi.sqlite3_deserialize(\n db,\n \"main\",\n dataPointer,\n snapshot.byteLength,\n snapshot.byteLength,\n capi.SQLITE_DESERIALIZE_FREEONCLOSE | capi.SQLITE_DESERIALIZE_RESIZEABLE,\n );\n db.checkRc(resultCode);\n },\n createScalarFunction: ({ name, callback, arity, deterministic, directOnly, innocuous }) => {\n db.createFunction({\n name,\n xFunc: (_, ...args) => callback(...(args as SqliteDriverValue[])) as SqlValue,\n arity,\n deterministic,\n directOnly,\n innocuous,\n });\n },\n close: () => {\n db.close();\n },\n };\n}\n","import type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { createWasmMemoryDriver } from \"../wasm-driver\";\nimport { type SchemaVerificationIssue, verifySyncSchemaWithDriver } from \"./verify-sync-schema\";\n\n/** {@link verifySyncSchemaWithDriver} on a throwaway in-memory sqlite-wasm database. */\nexport async function verifySyncSchema(schema: SyncDbSchema): Promise<SchemaVerificationIssue[]> {\n return verifySyncSchemaWithDriver(schema, await createWasmMemoryDriver());\n}\n","import type { ImportDataOptions, ImportDataResult, SyncedDbExport } from \"../export-import\";\nimport type {\n DeSyncDetectedReason,\n EventsPullRequest,\n EventsPushRequest,\n EventsPushResponse,\n} from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { CrdtEventType } from \"../sqlite-crdt/crdt-table-schema\";\nimport type { ExecuteParams, ExecuteResult } from \"../sqlite-db-wrapper\";\nimport { TypedBroadcastChannel } from \"../utils\";\n\nexport const syncDbWorkerLockName = \"sync-db-worker-lock\";\nexport const syncDbClientLockName = \"sync-db-client-lock\";\n\nexport type WorkerNotificationMessage =\n | {\n notificationType: \"new-event-chunk-applied\";\n newSyncId: number;\n /** Worker's state hash after applying up to `newSyncId`. */\n stateHash: string | null;\n }\n | {\n notificationType: \"state-changed\";\n state: WorkerState;\n /** Id of the worker's persisted database. A wiped database gets a new one. */\n storageGeneration: string;\n }\n | {\n notificationType: \"reload-requested\";\n reloadEpoch: string;\n clean: boolean;\n }\n | {\n notificationType: \"de-sync-detected\";\n reason: DeSyncDetectedReason;\n }\n | {\n notificationType: \"remote-schema-version-mismatch\";\n remoteSchemaVersion: number;\n localSchemaVersion: number;\n };\n\nexport type WorkerNotificationEvents = {\n [K in WorkerNotificationMessage[\"notificationType\"]]: Extract<WorkerNotificationMessage, { notificationType: K }>;\n};\n\nexport type WorkerState = {\n remoteState: \"online\" | \"offline\" | \"connecting\";\n deSynced: boolean;\n schemaVersionMismatched: boolean;\n};\n\nexport type GetSnapshotResponse = {\n file: Uint8Array<ArrayBufferLike>;\n syncId: number;\n schemaVersion: number;\n storageGeneration: string;\n};\n\nexport type EventsPullResponse = {\n events: {\n schema_version: number;\n type: CrdtEventType;\n timestamp: string;\n dataset: string;\n item_id: string;\n payload: string;\n }[];\n hasMore: boolean;\n nextSyncId: number;\n};\n\nexport interface WorkerRpc {\n getSnapshot: () => GetSnapshotResponse;\n importData: (data: SyncedDbExport, opts?: ImportDataOptions) => ImportDataResult | Promise<ImportDataResult>;\n pushTabEvents: (request: EventsPushRequest) => EventsPushResponse;\n execute: (query: ExecuteParams) => ExecuteResult<unknown>;\n pullEvents: (params: EventsPullRequest) => EventsPullResponse;\n postState: () => void;\n goOnline: () => Promise<void>;\n goOffline: () => void;\n dropConnection: () => void;\n sync: () => void;\n requestReload: (options: { clean: boolean }) => Promise<void>;\n}\n\nexport type WorkerRequestMethod = keyof WorkerRpc;\n\nexport type WorkerRequestMessage<TMethod extends WorkerRequestMethod = WorkerRequestMethod> = {\n type: \"request\";\n requestId: string;\n method: TMethod;\n args: Parameters<WorkerRpc[TMethod]>;\n};\n\nexport type WorkerResponseMessage<TMethod extends WorkerRequestMethod = WorkerRequestMethod> = {\n type: \"response\";\n requestId: string;\n data: ReturnType<WorkerRpc[TMethod]>;\n};\n\nexport type WorkerErrorResponseMessage = {\n type: \"error-response\";\n requestId: string;\n error: string;\n};\n\nexport type AsyncRpc<T> = {\n [K in keyof T]: T[K] extends (...args: infer U) => infer V ? (...args: U) => Promise<Awaited<V>> : never;\n};\n\nexport const broadcastChannelNames = {\n requests: \"sync-db-worker-requests\",\n responses: \"sync-db-worker-responses\",\n} as const;\n\nexport type WorkerBroadcastChannels = {\n requests: TypedBroadcastChannel<WorkerRequestMessage>;\n responses: TypedBroadcastChannel<WorkerResponseMessage | WorkerErrorResponseMessage | WorkerNotificationMessage>;\n};\n\nexport const createBroadcastChannels = (prefix: string): WorkerBroadcastChannels => {\n return {\n requests: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.requests}`),\n responses: new TypedBroadcastChannel(`${prefix}-${broadcastChannelNames.responses}`),\n };\n};\n\nexport type WorkerConfig<Props = any> = {\n dbId: string;\n clientId: string;\n props: Props;\n};\n\nexport type WorkerInitMessage = {\n type: \"init\";\n config: WorkerConfig;\n};\n\nexport function isWorkerInitMessage(message: unknown): message is WorkerInitMessage {\n return typeof message === \"object\" && message !== null && \"type\" in message && message.type === \"init\";\n}\n\nexport function isWorkerRequestMessage(message: unknown): message is WorkerRequestMessage {\n return typeof message === \"object\" && message !== null && \"type\" in message && message.type === \"request\";\n}\n\nexport function isWorkerResponseMessage(message: unknown): message is WorkerResponseMessage {\n return (\n typeof message === \"object\" && message !== null && \"type\" in message && \"requestId\" in message && \"data\" in message\n );\n}\n\nexport function isWorkerErrorResponseMessage(message: unknown): message is WorkerErrorResponseMessage {\n return typeof message === \"object\" && message !== null && \"type\" in message && message.type === \"error-response\";\n}\n\nexport function isWorkerNotificationMessage(message: unknown): message is WorkerNotificationMessage {\n return typeof message === \"object\" && message !== null && \"notificationType\" in message && !!message.notificationType;\n}\n","import { acquireLock } from \"../utils\";\n\nconst replicaLockName = (dbId: string) => `sync-db-replica-lock-${dbId}`;\n\nexport function acquireReplicaLock({ dbId }: { dbId: string }) {\n return acquireLock(replicaLockName(dbId), { mode: \"shared\" });\n}\n\nexport function runWithNoActiveReplicas({ dbId, run }: { dbId: string; run: () => void | Promise<void> }) {\n return navigator.locks.request(replicaLockName(dbId), { mode: \"exclusive\", ifAvailable: true }, async (lock) => {\n if (!lock) {\n return false;\n }\n await run();\n return true;\n });\n}\n","import { createStore, del, get, set } from \"idb-keyval\";\nimport { generateId } from \"../utils\";\nimport type { WorkerNotificationMessage } from \"./worker-common\";\n\nexport type ResetRequest = {\n epoch: string;\n requestedAt: number;\n};\n\n/** Durable async key-value storage for reset state. Injectable for tests. */\nexport type ResetStore = {\n get: <T>(key: string) => Promise<T | undefined>;\n set: (key: string, value: unknown) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/** Default IndexedDB-backed reset store (idb-keyval over a dedicated database). */\nexport function createIdbResetStore(): ResetStore {\n const store = createStore(\"sqlite-sync\", \"kv\");\n return {\n get: (key) => get(key, store),\n set: (key, value) => set(key, value, store),\n delete: (key) => del(key, store),\n };\n}\n\n/**\n * A clean reset is a recovery action for a de-sync detected now. If the reload\n * never happens (broadcast lost, tab crashed mid-reload, browser killed the page),\n * the request must not fire on an arbitrary later cold start and silently wipe\n * local-only writes accumulated since.\n */\nexport const RESET_REQUEST_TTL_MS = 10 * 60 * 1000; // 10 minutes\n\nconst resetRequestKey = (dbId: string) => `sqlite-sync-reset-request-${dbId}`;\nconst resetAppliedKey = (dbId: string) => `sqlite-sync-reset-applied-${dbId}`;\n\ntype ResetStateStoreOptions = {\n store: ResetStore;\n dbId: string;\n now?: () => number;\n};\n\nexport type ResetStateStore = ReturnType<typeof createResetStateStore>;\n\n/**\n * Worker-owned durable reset state. The reset decision must be owned by the\n * elected worker, not by tabs during `createSyncedDb` — otherwise a later\n * worker election could repeat an already-applied wipe.\n */\nexport function createResetStateStore({ store, dbId, now = () => Date.now() }: ResetStateStoreOptions) {\n const requestKey = resetRequestKey(dbId);\n const appliedKey = resetAppliedKey(dbId);\n\n return {\n async writeResetRequest(epoch: string): Promise<ResetRequest> {\n const request: ResetRequest = { epoch, requestedAt: now() };\n await store.set(requestKey, request);\n return request;\n },\n /**\n * Read the pending reset request after winning the worker election.\n * Returns the request only when it has not been applied yet and is within\n * the TTL. Stale requests are deleted so they cannot fire on a later cold start.\n */\n async resolvePendingReset(): Promise<ResetRequest | undefined> {\n const request = await store.get<ResetRequest>(requestKey);\n if (!request) {\n return undefined;\n }\n\n if (now() - request.requestedAt > RESET_REQUEST_TTL_MS) {\n await store.delete(requestKey);\n return undefined;\n }\n\n const appliedEpoch = await store.get<string>(appliedKey);\n if (request.epoch === appliedEpoch) {\n return undefined;\n }\n\n return request;\n },\n /**\n * Record the epoch as applied. Must be called only after the worker has\n * successfully initialized with `clearOnInit: true`, so a failed init can\n * be retried by a later elected worker.\n */\n async markResetApplied(epoch: string): Promise<void> {\n await store.set(appliedKey, epoch);\n },\n };\n}\n\ntype ReloadRequestHandlerOptions = {\n resetState: ResetStateStore;\n broadcast: (message: WorkerNotificationMessage) => void;\n generateEpoch?: () => string;\n};\n\n/**\n * Worker-side `requestReload` RPC implementation. For `clean: true` the reset\n * request is durably stored before broadcasting and before the RPC resolves,\n * so the epoch survives no matter which path triggers the reload.\n */\nexport function createReloadRequestHandler({\n resetState,\n broadcast,\n generateEpoch = generateId,\n}: ReloadRequestHandlerOptions) {\n return async (options: { clean: boolean }): Promise<void> => {\n const reloadEpoch = generateEpoch();\n\n if (options.clean) {\n await resetState.writeResetRequest(reloadEpoch);\n }\n\n broadcast({\n notificationType: \"reload-requested\",\n reloadEpoch,\n clean: options.clean,\n });\n };\n}\n"],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ct as jsonSafeParse, S as EventsPullResponse, U as ClientDatasetRejectionReason, _n as MigratableEvent, b as EventsPushResponse, bn as SyncDbMigrator, en as SyncDbSchema } from "./index-BHsKLqcK.js";
1
+ import { An as SyncDbMigrator, D as EventsPullResponse, Dn as MigratableEvent, J as ClientDatasetRejectionReason, Ot as jsonSafeParse, T as EventsPushResponse, un as SyncDbSchema } from "./index-CH4lf3iX.js";
2
2
  import { z } from "zod";
3
3
  //#region src/server/admit-client-push.d.ts
4
4
  type PushedCrdtEvent = MigratableEvent & {
@@ -92,10 +92,15 @@ type SyncServerMessage = {
92
92
  type: "events-applied";
93
93
  newSyncId: number;
94
94
  /**
95
- * Remote's HLC checksum after applying up to `newSyncId`. Used by clients
95
+ * Remote's state hash after applying up to `newSyncId`. Used by clients
96
96
  * to detect de-sync when they are caught up.
97
97
  */
98
- eventHlcSum: string | null;
98
+ stateHash: string | null;
99
+ /**
100
+ * Always null. Clients before the state hash compared this event checksum, and
101
+ * they only skip the comparison for an explicit null.
102
+ */
103
+ eventHlcSum: null;
99
104
  };
100
105
  type SyncServerRequest = z.infer<typeof syncServerZodSchema.request>;
101
106
  type ExtractSyncServerRequest<T extends SyncServerRequest["type"]> = Extract<SyncServerRequest, {
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as isNoOpCrdtEventPayload, t as CRDT_EVENT_NO_OP_PAYLOAD, u as jsonSafeParse } from "./crdt-table-schema-BRm_WdcW.js";
2
- import { r as validateNewCrdtEvent } from "./crdt-sync-remote-source-CPXzmSIS.js";
1
+ import { n as isNoOpCrdtEventPayload, t as CRDT_EVENT_NO_OP_PAYLOAD, u as jsonSafeParse } from "./crdt-table-schema-D6Yf61Ex.js";
2
+ import { i as validateNewCrdtEvent } from "./crdt-sync-remote-source-yeTEkOjH.js";
3
3
  import { r as getClientDatasetRejection } from "./admit-client-events-BDrJw4SK.js";
4
4
  import { n as HEARTBEAT_PONG, t as HEARTBEAT_PING } from "./heartbeat-CJWQAoGP.js";
5
5
  import { z } from "zod";
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server/admit-client-push.ts","../src/server/server-common.ts"],"sourcesContent":["import { DEFAULT_MAX_DRIFT_MS } from \"../hlc\";\nimport type { MigratableEvent, SyncDbMigrator } from \"../migrations/migrator\";\nimport { type ClientDatasetRejectionReason, getClientDatasetRejection } from \"../schema/admit-client-events\";\nimport { validateNewCrdtEvent } from \"../schema/validate-crdt-event\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { CRDT_EVENT_NO_OP_PAYLOAD, isNoOpCrdtEventPayload } from \"../sqlite-crdt/crdt-table-schema\";\n\nexport type PushedCrdtEvent = MigratableEvent & { timestamp: string };\n\nexport type PushRejectionReason =\n | \"invalid-timestamp\"\n | \"timestamp-too-far-in-future\"\n | \"invalid-schema-version\"\n | \"schema-version-too-new\"\n | \"invalid-payload\"\n | \"migration-failed\"\n | ClientDatasetRejectionReason\n | \"schema-validation-failed\";\n\nexport type PushRejection<T> = {\n event: T;\n reason: PushRejectionReason;\n errors?: string[];\n};\n\nexport type AdmitClientPushResult<T> = {\n admitted: T[];\n rejected: PushRejection<T>[];\n};\n\ntype AdmitClientPushOptions<T> = {\n syncDbSchema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">;\n migrator: Pick<SyncDbMigrator, \"migrateEvent\" | \"latestSchemaVersion\">;\n events: readonly T[];\n now: number;\n};\n\ntype AdmitEventResult<T> = { ok: true; event: T } | { ok: false; reason: PushRejectionReason; errors?: string[] };\n\nconst timestampPattern = /^\\d{15}:[0-9a-z]{5}:.+$/;\n\n/**\n * Checks a client push before the hub persists it. Admitted events are migrated to the latest\n * schema version, target a client-writable base table, and carry a payload that matches the\n * schema. No-op events only need a valid timestamp and schema version. Everything else is\n * rejected with a reason.\n */\nexport function admitClientPush<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>): AdmitClientPushResult<T> {\n const admitted: T[] = [];\n const rejected: PushRejection<T>[] = [];\n for (const event of opts.events) {\n const result = admitEvent(opts, event);\n if (result.ok) {\n admitted.push(result.event);\n } else {\n rejected.push({ event, reason: result.reason, ...(result.errors && { errors: result.errors }) });\n }\n }\n return { admitted, rejected };\n}\n\nfunction admitEvent<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>, event: T): AdmitEventResult<T> {\n const { migrator } = opts;\n\n if (!timestampPattern.test(event.timestamp)) {\n return { ok: false, reason: \"invalid-timestamp\" };\n }\n if (Number(event.timestamp.slice(0, 15)) - opts.now > DEFAULT_MAX_DRIFT_MS) {\n return { ok: false, reason: \"timestamp-too-far-in-future\" };\n }\n if (!Number.isInteger(event.schema_version) || event.schema_version < 0) {\n return { ok: false, reason: \"invalid-schema-version\" };\n }\n if (event.schema_version > migrator.latestSchemaVersion) {\n return { ok: false, reason: \"schema-version-too-new\" };\n }\n if (isNoOpCrdtEventPayload(event.payload)) {\n return { ok: true, event };\n }\n const payload = parseJsonObject(event.payload);\n if (!payload) {\n return { ok: false, reason: \"invalid-payload\" };\n }\n\n let migrated: T | null;\n try {\n migrated = migrator.migrateEvent({ ...event });\n } catch (error) {\n return { ok: false, reason: \"migration-failed\", errors: [error instanceof Error ? error.message : String(error)] };\n }\n if (!migrated) {\n return {\n ok: true,\n event: { ...event, schema_version: migrator.latestSchemaVersion, payload: CRDT_EVENT_NO_OP_PAYLOAD },\n };\n }\n\n const datasetRejection = getClientDatasetRejection(opts.syncDbSchema, migrated.dataset);\n if (datasetRejection) {\n return { ok: false, reason: datasetRejection };\n }\n\n const validation = validateNewCrdtEvent(opts.syncDbSchema, {\n ...migrated,\n payload: migrated.payload === event.payload ? payload : migrated.payload,\n });\n if (!validation.success) {\n return { ok: false, reason: \"schema-validation-failed\", errors: validation.errors };\n }\n\n return { ok: true, event: migrated };\n}\n\nfunction parseJsonObject(payload: string): Record<string, unknown> | null {\n try {\n const value: unknown = JSON.parse(payload);\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n","import { z } from \"zod\";\nimport { type EventsPushResponse, MAX_PUSH_EVENTS } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { EventsPullResponse } from \"../worker-db/worker-common\";\n\nconst pullEventsZodSchema = z.object({\n type: z.literal(\"pull-events\"),\n requestId: z.string(),\n afterSyncId: z.number(),\n excludeNodeId: z.string().optional(),\n});\nconst pushEventsZodSchema = z.object({\n type: z.literal(\"push-events\"),\n requestId: z.string(),\n nodeId: z.string(),\n events: z\n .array(\n z.object({\n schema_version: z.number(),\n timestamp: z.string(),\n type: z.enum([\"item-created\", \"item-updated\", \"item-deleted\"]),\n dataset: z.string(),\n item_id: z.string(),\n payload: z.string(),\n }),\n )\n .max(MAX_PUSH_EVENTS),\n});\n\nexport const syncServerZodSchema = {\n pullEvents: pullEventsZodSchema,\n pushEvents: pushEventsZodSchema,\n request: z.discriminatedUnion(\"type\", [pullEventsZodSchema, pushEventsZodSchema]),\n};\n\nexport type SyncServerMessage =\n | {\n type: \"events-pull-response\";\n requestId: string;\n data: EventsPullResponse;\n }\n | {\n type: \"events-push-response\";\n requestId: string;\n data: EventsPushResponse;\n }\n | {\n type: \"events-applied\";\n newSyncId: number;\n /**\n * Remote's HLC checksum after applying up to `newSyncId`. Used by clients\n * to detect de-sync when they are caught up.\n */\n eventHlcSum: string | null;\n };\n\nexport type SyncServerRequest = z.infer<typeof syncServerZodSchema.request>;\n\nexport type ExtractSyncServerRequest<T extends SyncServerRequest[\"type\"]> = Extract<SyncServerRequest, { type: T }>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server/admit-client-push.ts","../src/server/server-common.ts"],"sourcesContent":["import { DEFAULT_MAX_DRIFT_MS } from \"../hlc\";\nimport type { MigratableEvent, SyncDbMigrator } from \"../migrations/migrator\";\nimport { type ClientDatasetRejectionReason, getClientDatasetRejection } from \"../schema/admit-client-events\";\nimport { validateNewCrdtEvent } from \"../schema/validate-crdt-event\";\nimport type { SyncDbSchema } from \"../sqlite-crdt/crdt-schema\";\nimport { CRDT_EVENT_NO_OP_PAYLOAD, isNoOpCrdtEventPayload } from \"../sqlite-crdt/crdt-table-schema\";\n\nexport type PushedCrdtEvent = MigratableEvent & { timestamp: string };\n\nexport type PushRejectionReason =\n | \"invalid-timestamp\"\n | \"timestamp-too-far-in-future\"\n | \"invalid-schema-version\"\n | \"schema-version-too-new\"\n | \"invalid-payload\"\n | \"migration-failed\"\n | ClientDatasetRejectionReason\n | \"schema-validation-failed\";\n\nexport type PushRejection<T> = {\n event: T;\n reason: PushRejectionReason;\n errors?: string[];\n};\n\nexport type AdmitClientPushResult<T> = {\n admitted: T[];\n rejected: PushRejection<T>[];\n};\n\ntype AdmitClientPushOptions<T> = {\n syncDbSchema: Pick<SyncDbSchema, \"tables\" | \"tablesConfig\">;\n migrator: Pick<SyncDbMigrator, \"migrateEvent\" | \"latestSchemaVersion\">;\n events: readonly T[];\n now: number;\n};\n\ntype AdmitEventResult<T> = { ok: true; event: T } | { ok: false; reason: PushRejectionReason; errors?: string[] };\n\nconst timestampPattern = /^\\d{15}:[0-9a-z]{5}:.+$/;\n\n/**\n * Checks a client push before the hub persists it. Admitted events are migrated to the latest\n * schema version, target a client-writable base table, and carry a payload that matches the\n * schema. No-op events only need a valid timestamp and schema version. Everything else is\n * rejected with a reason.\n */\nexport function admitClientPush<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>): AdmitClientPushResult<T> {\n const admitted: T[] = [];\n const rejected: PushRejection<T>[] = [];\n for (const event of opts.events) {\n const result = admitEvent(opts, event);\n if (result.ok) {\n admitted.push(result.event);\n } else {\n rejected.push({ event, reason: result.reason, ...(result.errors && { errors: result.errors }) });\n }\n }\n return { admitted, rejected };\n}\n\nfunction admitEvent<T extends PushedCrdtEvent>(opts: AdmitClientPushOptions<T>, event: T): AdmitEventResult<T> {\n const { migrator } = opts;\n\n if (!timestampPattern.test(event.timestamp)) {\n return { ok: false, reason: \"invalid-timestamp\" };\n }\n if (Number(event.timestamp.slice(0, 15)) - opts.now > DEFAULT_MAX_DRIFT_MS) {\n return { ok: false, reason: \"timestamp-too-far-in-future\" };\n }\n if (!Number.isInteger(event.schema_version) || event.schema_version < 0) {\n return { ok: false, reason: \"invalid-schema-version\" };\n }\n if (event.schema_version > migrator.latestSchemaVersion) {\n return { ok: false, reason: \"schema-version-too-new\" };\n }\n if (isNoOpCrdtEventPayload(event.payload)) {\n return { ok: true, event };\n }\n const payload = parseJsonObject(event.payload);\n if (!payload) {\n return { ok: false, reason: \"invalid-payload\" };\n }\n\n let migrated: T | null;\n try {\n migrated = migrator.migrateEvent({ ...event });\n } catch (error) {\n return { ok: false, reason: \"migration-failed\", errors: [error instanceof Error ? error.message : String(error)] };\n }\n if (!migrated) {\n return {\n ok: true,\n event: { ...event, schema_version: migrator.latestSchemaVersion, payload: CRDT_EVENT_NO_OP_PAYLOAD },\n };\n }\n\n const datasetRejection = getClientDatasetRejection(opts.syncDbSchema, migrated.dataset);\n if (datasetRejection) {\n return { ok: false, reason: datasetRejection };\n }\n\n const validation = validateNewCrdtEvent(opts.syncDbSchema, {\n ...migrated,\n payload: migrated.payload === event.payload ? payload : migrated.payload,\n });\n if (!validation.success) {\n return { ok: false, reason: \"schema-validation-failed\", errors: validation.errors };\n }\n\n return { ok: true, event: migrated };\n}\n\nfunction parseJsonObject(payload: string): Record<string, unknown> | null {\n try {\n const value: unknown = JSON.parse(payload);\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n","import { z } from \"zod\";\nimport { type EventsPushResponse, MAX_PUSH_EVENTS } from \"../sqlite-crdt/crdt-sync-remote-source\";\nimport type { EventsPullResponse } from \"../worker-db/worker-common\";\n\nconst pullEventsZodSchema = z.object({\n type: z.literal(\"pull-events\"),\n requestId: z.string(),\n afterSyncId: z.number(),\n excludeNodeId: z.string().optional(),\n});\nconst pushEventsZodSchema = z.object({\n type: z.literal(\"push-events\"),\n requestId: z.string(),\n nodeId: z.string(),\n events: z\n .array(\n z.object({\n schema_version: z.number(),\n timestamp: z.string(),\n type: z.enum([\"item-created\", \"item-updated\", \"item-deleted\"]),\n dataset: z.string(),\n item_id: z.string(),\n payload: z.string(),\n }),\n )\n .max(MAX_PUSH_EVENTS),\n});\n\nexport const syncServerZodSchema = {\n pullEvents: pullEventsZodSchema,\n pushEvents: pushEventsZodSchema,\n request: z.discriminatedUnion(\"type\", [pullEventsZodSchema, pushEventsZodSchema]),\n};\n\nexport type SyncServerMessage =\n | {\n type: \"events-pull-response\";\n requestId: string;\n data: EventsPullResponse;\n }\n | {\n type: \"events-push-response\";\n requestId: string;\n data: EventsPushResponse;\n }\n | {\n type: \"events-applied\";\n newSyncId: number;\n /**\n * Remote's state hash after applying up to `newSyncId`. Used by clients\n * to detect de-sync when they are caught up.\n */\n stateHash: string | null;\n /**\n * Always null. Clients before the state hash compared this event checksum, and\n * they only skip the comparison for an explicit null.\n */\n eventHlcSum: null;\n };\n\nexport type SyncServerRequest = z.infer<typeof syncServerZodSchema.request>;\n\nexport type ExtractSyncServerRequest<T extends SyncServerRequest[\"type\"]> = Extract<SyncServerRequest, { type: T }>;\n"],"mappings":";;;;;;AAuCA,MAAM,mBAAmB;;;;;;;AAQzB,SAAgB,gBAA2C,MAA2D;CACpH,MAAM,WAAgB,CAAC;CACvB,MAAM,WAA+B,CAAC;CACtC,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC/B,MAAM,SAAS,WAAW,MAAM,KAAK;EACrC,IAAI,OAAO,IACT,SAAS,KAAK,OAAO,KAAK;OAE1B,SAAS,KAAK;GAAE;GAAO,QAAQ,OAAO;GAAQ,GAAI,OAAO,UAAU,EAAE,QAAQ,OAAO,OAAO;EAAG,CAAC;CAEnG;CACA,OAAO;EAAE;EAAU;CAAS;AAC9B;AAEA,SAAS,WAAsC,MAAiC,OAA+B;CAC7G,MAAM,EAAE,aAAa;CAErB,IAAI,CAAC,iBAAiB,KAAK,MAAM,SAAS,GACxC,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAoB;CAElD,IAAI,OAAO,MAAM,UAAU,MAAM,GAAG,EAAE,CAAC,IAAI,KAAK,MAAA,OAC9C,OAAO;EAAE,IAAI;EAAO,QAAQ;CAA8B;CAE5D,IAAI,CAAC,OAAO,UAAU,MAAM,cAAc,KAAK,MAAM,iBAAiB,GACpE,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAyB;CAEvD,IAAI,MAAM,iBAAiB,SAAS,qBAClC,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAyB;CAEvD,IAAI,uBAAuB,MAAM,OAAO,GACtC,OAAO;EAAE,IAAI;EAAM;CAAM;CAE3B,MAAM,UAAU,gBAAgB,MAAM,OAAO;CAC7C,IAAI,CAAC,SACH,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAkB;CAGhD,IAAI;CACJ,IAAI;EACF,WAAW,SAAS,aAAa,EAAE,GAAG,MAAM,CAAC;CAC/C,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO,QAAQ;GAAoB,QAAQ,CAAC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;EAAE;CACnH;CACA,IAAI,CAAC,UACH,OAAO;EACL,IAAI;EACJ,OAAO;GAAE,GAAG;GAAO,gBAAgB,SAAS;GAAqB,SAAS;EAAyB;CACrG;CAGF,MAAM,mBAAmB,0BAA0B,KAAK,cAAc,SAAS,OAAO;CACtF,IAAI,kBACF,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAiB;CAG/C,MAAM,aAAa,qBAAqB,KAAK,cAAc;EACzD,GAAG;EACH,SAAS,SAAS,YAAY,MAAM,UAAU,UAAU,SAAS;CACnE,CAAC;CACD,IAAI,CAAC,WAAW,SACd,OAAO;EAAE,IAAI;EAAO,QAAQ;EAA4B,QAAQ,WAAW;CAAO;CAGpF,OAAO;EAAE,IAAI;EAAM,OAAO;CAAS;AACrC;AAEA,SAAS,gBAAgB,SAAiD;CACxE,IAAI;EACF,MAAM,QAAiB,KAAK,MAAM,OAAO;EACzC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,IACrE,QACD;CACN,QAAQ;EACN,OAAO;CACT;AACF;;;ACtHA,MAAM,sBAAsB,EAAE,OAAO;CACnC,MAAM,EAAE,QAAQ,aAAa;CAC7B,WAAW,EAAE,OAAO;CACpB,aAAa,EAAE,OAAO;CACtB,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS;AACrC,CAAC;AACD,MAAM,sBAAsB,EAAE,OAAO;CACnC,MAAM,EAAE,QAAQ,aAAa;CAC7B,WAAW,EAAE,OAAO;CACpB,QAAQ,EAAE,OAAO;CACjB,QAAQ,EACL,MACC,EAAE,OAAO;EACP,gBAAgB,EAAE,OAAO;EACzB,WAAW,EAAE,OAAO;EACpB,MAAM,EAAE,KAAK;GAAC;GAAgB;GAAgB;EAAc,CAAC;EAC7D,SAAS,EAAE,OAAO;EAClB,SAAS,EAAE,OAAO;EAClB,SAAS,EAAE,OAAO;CACpB,CAAC,CACH,CAAC,CACA,IAAA,GAAmB;AACxB,CAAC;AAED,MAAa,sBAAsB;CACjC,YAAY;CACZ,YAAY;CACZ,SAAS,EAAE,mBAAmB,QAAQ,CAAC,qBAAqB,mBAAmB,CAAC;AAClF"}