@korajs/store 1.0.0-beta.0 → 1.0.0-beta.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.
Files changed (61) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +39 -5
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +1 -1
  6. package/dist/adapters/indexeddb.d.ts +1 -1
  7. package/dist/adapters/indexeddb.js +13 -6
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-worker.cjs +25 -3
  10. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  11. package/dist/adapters/sqlite-wasm-worker.js +25 -3
  12. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  13. package/dist/adapters/sqlite-wasm.cjs +29 -0
  14. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  15. package/dist/adapters/sqlite-wasm.d.cts +17 -2
  16. package/dist/adapters/sqlite-wasm.d.ts +17 -2
  17. package/dist/adapters/sqlite-wasm.js +1 -1
  18. package/dist/{backup-5XZ7THNJ.js → backup-IQE4QC7Q.js} +3 -3
  19. package/dist/blob-fs.cjs +166 -0
  20. package/dist/blob-fs.cjs.map +1 -0
  21. package/dist/blob-fs.d.cts +29 -0
  22. package/dist/blob-fs.d.ts +29 -0
  23. package/dist/blob-fs.js +128 -0
  24. package/dist/blob-fs.js.map +1 -0
  25. package/dist/{chunk-OUQDC5Z6.js → chunk-2VRPF25R.js} +58 -38
  26. package/dist/chunk-2VRPF25R.js.map +1 -0
  27. package/dist/{chunk-ETDJEH2I.js → chunk-6NKLDBTY.js} +11 -1
  28. package/dist/{chunk-ETDJEH2I.js.map → chunk-6NKLDBTY.js.map} +1 -1
  29. package/dist/{chunk-5R4QMY3Z.js → chunk-OCOGQRHW.js} +13 -10
  30. package/dist/chunk-OCOGQRHW.js.map +1 -0
  31. package/dist/{chunk-RBPVEAVM.js → chunk-QUJGXSZ6.js} +30 -1
  32. package/dist/chunk-QUJGXSZ6.js.map +1 -0
  33. package/dist/{chunk-EIVXWF4B.js → chunk-RCTG3IJG.js} +4 -3
  34. package/dist/chunk-RCTG3IJG.js.map +1 -0
  35. package/dist/chunk-W4MTTNEZ.js +55 -0
  36. package/dist/chunk-W4MTTNEZ.js.map +1 -0
  37. package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
  38. package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
  39. package/dist/{export-audit-VPGQWNR6.js → export-audit-MT2LTMIN.js} +3 -3
  40. package/dist/index.cjs +765 -128
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +397 -6
  43. package/dist/index.d.ts +397 -6
  44. package/dist/index.js +603 -60
  45. package/dist/index.js.map +1 -1
  46. package/dist/internal.cjs +68 -43
  47. package/dist/internal.cjs.map +1 -1
  48. package/dist/internal.d.cts +3 -3
  49. package/dist/internal.d.ts +3 -3
  50. package/dist/internal.js +3 -3
  51. package/dist/{types-CyjXwrjW.d.cts → types-5h4Hqcwc.d.ts} +10 -3
  52. package/dist/{types-DAxCladl.d.ts → types-DfPiPwKr.d.cts} +10 -3
  53. package/dist/{types-CJOVKxLw.d.cts → types-k6AZgiH4.d.cts} +6 -1
  54. package/dist/{types-CJOVKxLw.d.ts → types-k6AZgiH4.d.ts} +6 -1
  55. package/package.json +13 -2
  56. package/dist/chunk-5R4QMY3Z.js.map +0 -1
  57. package/dist/chunk-EIVXWF4B.js.map +0 -1
  58. package/dist/chunk-OUQDC5Z6.js.map +0 -1
  59. package/dist/chunk-RBPVEAVM.js.map +0 -1
  60. /package/dist/{backup-5XZ7THNJ.js.map → backup-IQE4QC7Q.js.map} +0 -0
  61. /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-MT2LTMIN.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/indexeddb-persistence-scheduler.ts","../../src/adapters/sqlite-wasm-persistence.ts","../../src/adapters/indexeddb-adapter.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n\nconst DEFAULT_DEBOUNCE_MS = 500\n\nexport interface IndexedDbPersistenceSchedulerOptions {\n\t/** Debounce interval before writing a snapshot. Defaults to 500ms. */\n\tdebounceMs?: number\n\t/** Persist the in-memory database to IndexedDB. */\n\tflush: () => Promise<void>\n\t/** Called when persistence fails (mutation already committed in memory). */\n\tonError?: (error: unknown) => void\n}\n\n/**\n * Coalesces IndexedDB snapshot writes: debounces rapid mutations and flushes\n * immediately on tab hide (`visibilitychange`) or explicit {@link flushNow}.\n */\nexport class IndexedDbPersistenceScheduler {\n\tprivate readonly debounceMs: number\n\tprivate readonly flush: () => Promise<void>\n\tprivate readonly onError: ((error: unknown) => void) | undefined\n\tprivate timer: ReturnType<typeof setTimeout> | null = null\n\tprivate inFlight: Promise<void> | null = null\n\tprivate disposed = false\n\tprivate readonly onVisibilityChange: () => void\n\n\tconstructor(options: IndexedDbPersistenceSchedulerOptions) {\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS\n\t\tthis.flush = options.flush\n\t\tthis.onError = options.onError\n\t\tthis.onVisibilityChange = () => {\n\t\t\tif (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n\t\t\t\tvoid this.flushNow()\n\t\t\t}\n\t\t}\n\t\tif (typeof document !== 'undefined') {\n\t\t\tdocument.addEventListener('visibilitychange', this.onVisibilityChange)\n\t\t}\n\t}\n\n\t/** Schedule a debounced snapshot write. */\n\tschedule(): void {\n\t\tif (this.disposed) return\n\t\tif (this.debounceMs <= 0) {\n\t\t\tvoid this.flushNow()\n\t\t\treturn\n\t\t}\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t}\n\t\tthis.timer = setTimeout(() => {\n\t\t\tthis.timer = null\n\t\t\tvoid this.flushNow()\n\t\t}, this.debounceMs)\n\t}\n\n\t/** Cancel any pending debounce and persist immediately. */\n\tasync flushNow(): Promise<void> {\n\t\tif (this.disposed) return\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tthis.timer = null\n\t\t}\n\t\tif (this.inFlight) {\n\t\t\tawait this.inFlight\n\t\t\treturn\n\t\t}\n\t\tthis.inFlight = this.runFlush()\n\t\ttry {\n\t\t\tawait this.inFlight\n\t\t} finally {\n\t\t\tthis.inFlight = null\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tthis.timer = null\n\t\t}\n\t\tif (typeof document !== 'undefined') {\n\t\t\tdocument.removeEventListener('visibilitychange', this.onVisibilityChange)\n\t\t}\n\t}\n\n\tprivate async runFlush(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.flush()\n\t\t} catch (error) {\n\t\t\tthis.onError?.(error)\n\t\t}\n\t}\n}\n","/// <reference lib=\"dom\" />\nimport { PersistenceError } from '../errors'\n\nconst IDB_DATABASE_NAME = 'kora-persistence'\nconst IDB_STORE_NAME = 'databases'\nconst IDB_VERSION = 1\n\nconst DUMP_SUFFIX = '::dump'\n\n/**\n * Returns true when an IndexedDB error indicates storage quota was exceeded.\n */\nexport function isIndexedDbQuotaError(error: unknown): boolean {\n\tif (error instanceof DOMException && error.name === 'QuotaExceededError') {\n\t\treturn true\n\t}\n\tif (error instanceof PersistenceError && error.context?.quotaExceeded === true) {\n\t\treturn true\n\t}\n\tif (error instanceof Error && /quota/i.test(error.message)) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunction rejectWithIdbError(\n\treject: (reason: unknown) => void,\n\tmessage: string,\n\tcontext: Record<string, unknown>,\n\tevent: Event,\n): void {\n\tconst domError = (event.target as IDBRequest | IDBTransaction | undefined)?.error\n\tif (domError && isIndexedDbQuotaError(domError)) {\n\t\treject(\n\t\t\tnew PersistenceError(message, {\n\t\t\t\t...context,\n\t\t\t\tquotaExceeded: true,\n\t\t\t\tcause: domError.message,\n\t\t\t}),\n\t\t)\n\t\treturn\n\t}\n\treject(new PersistenceError(message, context))\n}\n\n/**\n * Open the IndexedDB database used for SQLite persistence.\n * Creates the object store on first access.\n */\nfunction openIdb(): Promise<IDBDatabase> {\n\treturn new Promise<IDBDatabase>((resolve, reject) => {\n\t\tconst request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION)\n\t\trequest.onupgradeneeded = () => {\n\t\t\tconst db = request.result\n\t\t\tif (!db.objectStoreNames.contains(IDB_STORE_NAME)) {\n\t\t\t\tdb.createObjectStore(IDB_STORE_NAME)\n\t\t\t}\n\t\t}\n\t\trequest.onsuccess = () => resolve(request.result)\n\t\trequest.onerror = () =>\n\t\t\treject(\n\t\t\t\tnew PersistenceError(\n\t\t\t\t\t`Failed to open IndexedDB: ${request.error?.message ?? 'unknown error'}`,\n\t\t\t\t\t{ database: IDB_DATABASE_NAME },\n\t\t\t\t),\n\t\t\t)\n\t})\n}\n\n/**\n * Save a serialized SQLite database to IndexedDB.\n *\n * @param dbName - Key under which to store the data\n * @param data - Serialized database as Uint8Array\n */\nexport async function saveToIndexedDB(dbName: string, data: Uint8Array): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.put(data, dbName)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = (event) =>\n\t\t\t\trejectWithIdbError(\n\t\t\t\t\treject,\n\t\t\t\t\t`Failed to save database \"${dbName}\" to IndexedDB`,\n\t\t\t\t\t{ dbName },\n\t\t\t\t\tevent,\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Save a logical SQL dump payload to IndexedDB for import-fallback restore.\n */\nexport async function saveDumpToIndexedDB(dbName: string, dump: unknown): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.put(dump, `${dbName}${DUMP_SUFFIX}`)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = (event) =>\n\t\t\t\trejectWithIdbError(\n\t\t\t\t\treject,\n\t\t\t\t\t`Failed to save dump for database \"${dbName}\"`,\n\t\t\t\t\t{ dbName },\n\t\t\t\t\tevent,\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Load a serialized SQLite database from IndexedDB.\n *\n * @param dbName - Key under which the data was stored\n * @returns The serialized database, or null if not found\n */\nexport async function loadFromIndexedDB(dbName: string): Promise<Uint8Array | null> {\n\tconst idb = await openIdb()\n\ttry {\n\t\treturn await new Promise<Uint8Array | null>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readonly')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tconst request = store.get(dbName)\n\t\t\trequest.onsuccess = () => {\n\t\t\t\tconst result = request.result\n\t\t\t\tif (result instanceof Uint8Array) {\n\t\t\t\t\tresolve(result)\n\t\t\t\t} else if (result) {\n\t\t\t\t\t// Handle ArrayBuffer or other typed array forms\n\t\t\t\t\tresolve(new Uint8Array(result as ArrayBuffer))\n\t\t\t\t} else {\n\t\t\t\t\tresolve(null)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequest.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to load database \"${dbName}\" from IndexedDB`, { dbName }),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Load a logical SQL dump payload from IndexedDB.\n */\nexport async function loadDumpFromIndexedDB<T>(dbName: string): Promise<T | null> {\n\tconst idb = await openIdb()\n\ttry {\n\t\treturn await new Promise<T | null>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readonly')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tconst request = store.get(`${dbName}${DUMP_SUFFIX}`)\n\t\t\trequest.onsuccess = () => {\n\t\t\t\tresolve((request.result as T | undefined) ?? null)\n\t\t\t}\n\t\t\trequest.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to load dump for database \"${dbName}\" from IndexedDB`, {\n\t\t\t\t\t\tdbName,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Delete a serialized SQLite database from IndexedDB.\n *\n * @param dbName - Key to delete\n */\nexport async function deleteFromIndexedDB(dbName: string): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.delete(dbName)\n\t\t\tstore.delete(`${dbName}${DUMP_SUFFIX}`)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to delete database \"${dbName}\" from IndexedDB`, { dbName }),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n","import type { KoraEventEmitter, SchemaDefinition } from '@korajs/core'\nimport { PersistenceError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { IndexedDbPersistenceScheduler } from './indexeddb-persistence-scheduler'\nimport { SqliteWasmAdapter } from './sqlite-wasm-adapter'\nimport type { WorkerBridge } from './sqlite-wasm-channel'\nimport {\n\tisIndexedDbQuotaError,\n\tloadDumpFromIndexedDB,\n\tloadFromIndexedDB,\n\tsaveDumpToIndexedDB,\n\tsaveToIndexedDB,\n} from './sqlite-wasm-persistence'\n\ninterface DatabaseDump {\n\ttables: Array<{\n\t\tname: string\n\t\tcolumns: string[]\n\t\trows: Array<Record<string, unknown>>\n\t}>\n}\n\n/**\n * Options for creating an IndexedDbAdapter.\n */\nexport interface IndexedDbAdapterOptions {\n\t/**\n\t * Database name used as the IndexedDB key for persistence.\n\t * Defaults to 'kora-db'.\n\t */\n\tdbName?: string\n\n\t/**\n\t * Injected WorkerBridge for testing. If omitted, a WebWorkerBridge is created\n\t * in browser environments.\n\t */\n\tbridge?: WorkerBridge\n\n\t/**\n\t * URL to the sqlite-wasm-worker script. Required in browsers if no bridge is provided.\n\t */\n\tworkerUrl?: string | URL\n\n\t/** Timeout for worker / follower RPC responses. Defaults to 30000ms. */\n\tworkerResponseTimeoutMs?: number\n\n\t/**\n\t * Debounce interval (ms) before writing snapshots to IndexedDB. Defaults to 500.\n\t */\n\tpersistenceDebounceMs?: number\n\n\t/**\n\t * When set, persistence failures and quota errors are emitted on this emitter.\n\t */\n\temitter?: KoraEventEmitter\n}\n\n/**\n * IndexedDB-backed adapter that uses SQLite WASM in-memory and serializes\n * the entire database to IndexedDB after mutations (coalesced/debounced).\n *\n * This is the fallback adapter for browsers where OPFS is not available.\n * It provides the same SQL interface as SqliteWasmAdapter, but persists by\n * serializing the full SQLite database to a single IndexedDB blob.\n *\n * @example\n * ```typescript\n * const adapter = new IndexedDbAdapter({ workerUrl: '/sqlite-wasm-worker.js' })\n * ```\n */\nexport class IndexedDbAdapter implements StorageAdapter {\n\tprivate inner: SqliteWasmAdapter\n\tprivate readonly dbName: string\n\tprivate readonly emitter: KoraEventEmitter | undefined\n\tprivate readonly scheduler: IndexedDbPersistenceScheduler\n\n\tconstructor(options: IndexedDbAdapterOptions = {}) {\n\t\tthis.dbName = options.dbName ?? 'kora-db'\n\t\tthis.emitter = options.emitter\n\t\tthis.inner = new SqliteWasmAdapter({\n\t\t\tbridge: options.bridge,\n\t\t\tworkerUrl: options.workerUrl,\n\t\t\tdbName: this.dbName,\n\t\t\tworkerResponseTimeoutMs: options.workerResponseTimeoutMs,\n\t\t})\n\t\tthis.scheduler = new IndexedDbPersistenceScheduler({\n\t\t\tdebounceMs: options.persistenceDebounceMs,\n\t\t\tflush: () => this.writeSnapshot(),\n\t\t\tonError: (error) => this.handlePersistenceError(error),\n\t\t})\n\t}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\tawait this.inner.open(schema)\n\n\t\tconst persisted = await loadFromIndexedDB(this.dbName)\n\t\tif (!persisted) return\n\n\t\ttry {\n\t\t\tawait this.inner.importDatabase(persisted)\n\t\t} catch {\n\t\t\tawait this.restoreFromDumpFallback()\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tawait this.scheduler.flushNow()\n\t\tthis.scheduler.dispose()\n\t\tawait this.inner.close()\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tawait this.inner.execute(sql, params)\n\t\tthis.scheduler.schedule()\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\treturn this.inner.query<T>(sql, params)\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tawait this.inner.transaction(fn)\n\t\tthis.scheduler.schedule()\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tawait this.inner.migrate(from, to, migration)\n\t\tthis.scheduler.schedule()\n\t}\n\n\t/**\n\t * Force an immediate snapshot write to IndexedDB (skips debounce).\n\t * Useful before tab unload or in tests.\n\t */\n\tasync flushPersistence(): Promise<void> {\n\t\tawait this.scheduler.flushNow()\n\t}\n\n\tprivate async writeSnapshot(): Promise<void> {\n\t\tconst data = await this.inner.exportDatabase()\n\t\tawait saveToIndexedDB(this.dbName, data)\n\t\tconst dump = await this.exportDump()\n\t\tawait saveDumpToIndexedDB(this.dbName, dump)\n\t}\n\n\tprivate handlePersistenceError(error: unknown): void {\n\t\tconst message = error instanceof Error ? error.message : 'IndexedDB persistence failed'\n\t\tconst code = error instanceof PersistenceError ? error.code : 'PERSISTENCE_FAILED'\n\t\tconst quotaExceeded = isIndexedDbQuotaError(error)\n\n\t\tif (quotaExceeded) {\n\t\t\tthis.emitter?.emit({\n\t\t\t\ttype: 'store:quota-exceeded',\n\t\t\t\tdbName: this.dbName,\n\t\t\t\tmessage,\n\t\t\t})\n\t\t}\n\n\t\tthis.emitter?.emit({\n\t\t\ttype: 'store:persistence-error',\n\t\t\tdbName: this.dbName,\n\t\t\tmessage,\n\t\t\tcode: quotaExceeded ? 'QUOTA_EXCEEDED' : code,\n\t\t})\n\t}\n\n\tprivate async restoreFromDumpFallback(): Promise<void> {\n\t\tconst dump = await loadDumpFromIndexedDB<DatabaseDump>(this.dbName)\n\t\tif (!dump) return\n\n\t\tfor (const table of dump.tables) {\n\t\t\tconst name = ensureSafeIdentifier(table.name)\n\t\t\tawait this.inner.execute(`DELETE FROM ${name}`)\n\n\t\t\tif (table.rows.length === 0) continue\n\n\t\t\tfor (const row of table.rows) {\n\t\t\t\tconst columns = table.columns.filter((column) =>\n\t\t\t\t\tObject.prototype.hasOwnProperty.call(row, column),\n\t\t\t\t)\n\t\t\t\tif (columns.length === 0) continue\n\n\t\t\t\tconst placeholders = columns.map(() => '?').join(', ')\n\t\t\t\tconst quotedColumns = columns.map((column) => ensureSafeIdentifier(column)).join(', ')\n\t\t\t\tconst values = columns.map((column) => row[column])\n\n\t\t\t\tawait this.inner.execute(\n\t\t\t\t\t`INSERT INTO ${name} (${quotedColumns}) VALUES (${placeholders})`,\n\t\t\t\t\tvalues,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async exportDump(): Promise<DatabaseDump> {\n\t\tconst tableRows = await this.inner.query<{ name: string }>(\n\t\t\t\"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n\t\t)\n\n\t\tconst tables: DatabaseDump['tables'] = []\n\t\tfor (const tableRow of tableRows) {\n\t\t\tconst tableName = ensureSafeIdentifier(tableRow.name)\n\t\t\tconst columns = await this.inner.query<{ name: string }>(`PRAGMA table_info(${tableName})`)\n\t\t\tconst columnNames = columns.map((column) => column.name)\n\t\t\tconst rows = await this.inner.query<Record<string, unknown>>(`SELECT * FROM ${tableName}`)\n\n\t\t\ttables.push({\n\t\t\t\tname: tableName,\n\t\t\t\tcolumns: columnNames,\n\t\t\t\trows,\n\t\t\t})\n\t\t}\n\n\t\treturn { tables }\n\t}\n}\n\nfunction ensureSafeIdentifier(identifier: string): string {\n\tif (!/^[a-zA-Z0-9_]+$/.test(identifier)) {\n\t\tthrow new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {\n\t\t\tcode: 'UNSAFE_IDENTIFIER',\n\t\t\tidentifier,\n\t\t})\n\t}\n\treturn identifier\n}\n"],"mappings":";;;;;;;;;;AAEA,IAAM,sBAAsB;AAerB,IAAM,gCAAN,MAAoC;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACT,QAA8C;AAAA,EAC9C,WAAiC;AAAA,EACjC,WAAW;AAAA,EACF;AAAA,EAEjB,YAAY,SAA+C;AAC1D,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ;AACvB,SAAK,qBAAqB,MAAM;AAC/B,UAAI,OAAO,aAAa,eAAe,SAAS,oBAAoB,UAAU;AAC7E,aAAK,KAAK,SAAS;AAAA,MACpB;AAAA,IACD;AACA,QAAI,OAAO,aAAa,aAAa;AACpC,eAAS,iBAAiB,oBAAoB,KAAK,kBAAkB;AAAA,IACtE;AAAA,EACD;AAAA;AAAA,EAGA,WAAiB;AAChB,QAAI,KAAK,SAAU;AACnB,QAAI,KAAK,cAAc,GAAG;AACzB,WAAK,KAAK,SAAS;AACnB;AAAA,IACD;AACA,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AAAA,IACxB;AACA,SAAK,QAAQ,WAAW,MAAM;AAC7B,WAAK,QAAQ;AACb,WAAK,KAAK,SAAS;AAAA,IACpB,GAAG,KAAK,UAAU;AAAA,EACnB;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC/B,QAAI,KAAK,SAAU;AACnB,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACd;AACA,QAAI,KAAK,UAAU;AAClB,YAAM,KAAK;AACX;AAAA,IACD;AACA,SAAK,WAAW,KAAK,SAAS;AAC9B,QAAI;AACH,YAAM,KAAK;AAAA,IACZ,UAAE;AACD,WAAK,WAAW;AAAA,IACjB;AAAA,EACD;AAAA,EAEA,UAAgB;AACf,SAAK,WAAW;AAChB,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACd;AACA,QAAI,OAAO,aAAa,aAAa;AACpC,eAAS,oBAAoB,oBAAoB,KAAK,kBAAkB;AAAA,IACzE;AAAA,EACD;AAAA,EAEA,MAAc,WAA0B;AACvC,QAAI;AACH,YAAM,KAAK,MAAM;AAAA,IAClB,SAAS,OAAO;AACf,WAAK,UAAU,KAAK;AAAA,IACrB;AAAA,EACD;AACD;;;AC1FA,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB;AACvB,IAAM,cAAc;AAEpB,IAAM,cAAc;AAKb,SAAS,sBAAsB,OAAyB;AAC9D,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,sBAAsB;AACzE,WAAO;AAAA,EACR;AACA,MAAI,iBAAiB,oBAAoB,MAAM,SAAS,kBAAkB,MAAM;AAC/E,WAAO;AAAA,EACR;AACA,MAAI,iBAAiB,SAAS,SAAS,KAAK,MAAM,OAAO,GAAG;AAC3D,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAEA,SAAS,mBACR,QACA,SACA,SACA,OACO;AACP,QAAM,WAAY,MAAM,QAAoD;AAC5E,MAAI,YAAY,sBAAsB,QAAQ,GAAG;AAChD;AAAA,MACC,IAAI,iBAAiB,SAAS;AAAA,QAC7B,GAAG;AAAA,QACH,eAAe;AAAA,QACf,OAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA;AAAA,EACD;AACA,SAAO,IAAI,iBAAiB,SAAS,OAAO,CAAC;AAC9C;AAMA,SAAS,UAAgC;AACxC,SAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACpD,UAAM,UAAU,UAAU,KAAK,mBAAmB,WAAW;AAC7D,YAAQ,kBAAkB,MAAM;AAC/B,YAAM,KAAK,QAAQ;AACnB,UAAI,CAAC,GAAG,iBAAiB,SAAS,cAAc,GAAG;AAClD,WAAG,kBAAkB,cAAc;AAAA,MACpC;AAAA,IACD;AACA,YAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,YAAQ,UAAU,MACjB;AAAA,MACC,IAAI;AAAA,QACH,6BAA6B,QAAQ,OAAO,WAAW,eAAe;AAAA,QACtE,EAAE,UAAU,kBAAkB;AAAA,MAC/B;AAAA,IACD;AAAA,EACF,CAAC;AACF;AAQA,eAAsB,gBAAgB,QAAgB,MAAiC;AACtF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,KAAK,IAAI,YAAY,gBAAgB,WAAW;AACtD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,IAAI,MAAM,MAAM;AACtB,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,CAAC,UACb;AAAA,QACC;AAAA,QACA,4BAA4B,MAAM;AAAA,QAClC,EAAE,OAAO;AAAA,QACT;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAKA,eAAsB,oBAAoB,QAAgB,MAA8B;AACvF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,KAAK,IAAI,YAAY,gBAAgB,WAAW;AACtD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,IAAI,MAAM,GAAG,MAAM,GAAG,WAAW,EAAE;AACzC,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,CAAC,UACb;AAAA,QACC;AAAA,QACA,qCAAqC,MAAM;AAAA,QAC3C,EAAE,OAAO;AAAA,QACT;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAQA,eAAsB,kBAAkB,QAA4C;AACnF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,WAAO,MAAM,IAAI,QAA2B,CAAC,SAAS,WAAW;AAChE,YAAM,KAAK,IAAI,YAAY,gBAAgB,UAAU;AACrD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,UAAU,MAAM,IAAI,MAAM;AAChC,cAAQ,YAAY,MAAM;AACzB,cAAM,SAAS,QAAQ;AACvB,YAAI,kBAAkB,YAAY;AACjC,kBAAQ,MAAM;AAAA,QACf,WAAW,QAAQ;AAElB,kBAAQ,IAAI,WAAW,MAAqB,CAAC;AAAA,QAC9C,OAAO;AACN,kBAAQ,IAAI;AAAA,QACb;AAAA,MACD;AACA,cAAQ,UAAU,MACjB;AAAA,QACC,IAAI,iBAAiB,4BAA4B,MAAM,oBAAoB,EAAE,OAAO,CAAC;AAAA,MACtF;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAKA,eAAsB,sBAAyB,QAAmC;AACjF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,WAAO,MAAM,IAAI,QAAkB,CAAC,SAAS,WAAW;AACvD,YAAM,KAAK,IAAI,YAAY,gBAAgB,UAAU;AACrD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,UAAU,MAAM,IAAI,GAAG,MAAM,GAAG,WAAW,EAAE;AACnD,cAAQ,YAAY,MAAM;AACzB,gBAAS,QAAQ,UAA4B,IAAI;AAAA,MAClD;AACA,cAAQ,UAAU,MACjB;AAAA,QACC,IAAI,iBAAiB,qCAAqC,MAAM,oBAAoB;AAAA,UACnF;AAAA,QACD,CAAC;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;;;AC3GO,IAAM,mBAAN,MAAiD;AAAA,EAC/C;AAAA,EACS;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAAmC,CAAC,GAAG;AAClD,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,UAAU,QAAQ;AACvB,SAAK,QAAQ,IAAI,kBAAkB;AAAA,MAClC,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,yBAAyB,QAAQ;AAAA,IAClC,CAAC;AACD,SAAK,YAAY,IAAI,8BAA8B;AAAA,MAClD,YAAY,QAAQ;AAAA,MACpB,OAAO,MAAM,KAAK,cAAc;AAAA,MAChC,SAAS,CAAC,UAAU,KAAK,uBAAuB,KAAK;AAAA,IACtD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,QAAyC;AACnD,UAAM,KAAK,MAAM,KAAK,MAAM;AAE5B,UAAM,YAAY,MAAM,kBAAkB,KAAK,MAAM;AACrD,QAAI,CAAC,UAAW;AAEhB,QAAI;AACH,YAAM,KAAK,MAAM,eAAe,SAAS;AAAA,IAC1C,QAAQ;AACP,YAAM,KAAK,wBAAwB;AAAA,IACpC;AAAA,EACD;AAAA,EAEA,MAAM,QAAuB;AAC5B,UAAM,KAAK,UAAU,SAAS;AAC9B,SAAK,UAAU,QAAQ;AACvB,UAAM,KAAK,MAAM,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,QAAQ,KAAa,QAAmC;AAC7D,UAAM,KAAK,MAAM,QAAQ,KAAK,MAAM;AACpC,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA,EAEA,MAAM,MAAS,KAAa,QAAkC;AAC7D,WAAO,KAAK,MAAM,MAAS,KAAK,MAAM;AAAA,EACvC;AAAA,EAEA,MAAM,YAAY,IAAuD;AACxE,UAAM,KAAK,MAAM,YAAY,EAAE;AAC/B,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA,EAEA,MAAM,QAAQ,MAAc,IAAY,WAAyC;AAChF,UAAM,KAAK,MAAM,QAAQ,MAAM,IAAI,SAAS;AAC5C,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,mBAAkC;AACvC,UAAM,KAAK,UAAU,SAAS;AAAA,EAC/B;AAAA,EAEA,MAAc,gBAA+B;AAC5C,UAAM,OAAO,MAAM,KAAK,MAAM,eAAe;AAC7C,UAAM,gBAAgB,KAAK,QAAQ,IAAI;AACvC,UAAM,OAAO,MAAM,KAAK,WAAW;AACnC,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAAA,EAC5C;AAAA,EAEQ,uBAAuB,OAAsB;AACpD,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;AAC9D,UAAM,gBAAgB,sBAAsB,KAAK;AAEjD,QAAI,eAAe;AAClB,WAAK,SAAS,KAAK;AAAA,QAClB,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,QACb;AAAA,MACD,CAAC;AAAA,IACF;AAEA,SAAK,SAAS,KAAK;AAAA,MAClB,MAAM;AAAA,MACN,QAAQ,KAAK;AAAA,MACb;AAAA,MACA,MAAM,gBAAgB,mBAAmB;AAAA,IAC1C,CAAC;AAAA,EACF;AAAA,EAEA,MAAc,0BAAyC;AACtD,UAAM,OAAO,MAAM,sBAAoC,KAAK,MAAM;AAClE,QAAI,CAAC,KAAM;AAEX,eAAW,SAAS,KAAK,QAAQ;AAChC,YAAM,OAAO,qBAAqB,MAAM,IAAI;AAC5C,YAAM,KAAK,MAAM,QAAQ,eAAe,IAAI,EAAE;AAE9C,UAAI,MAAM,KAAK,WAAW,EAAG;AAE7B,iBAAW,OAAO,MAAM,MAAM;AAC7B,cAAM,UAAU,MAAM,QAAQ;AAAA,UAAO,CAAC,WACrC,OAAO,UAAU,eAAe,KAAK,KAAK,MAAM;AAAA,QACjD;AACA,YAAI,QAAQ,WAAW,EAAG;AAE1B,cAAM,eAAe,QAAQ,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AACrD,cAAM,gBAAgB,QAAQ,IAAI,CAAC,WAAW,qBAAqB,MAAM,CAAC,EAAE,KAAK,IAAI;AACrF,cAAM,SAAS,QAAQ,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC;AAElD,cAAM,KAAK,MAAM;AAAA,UAChB,eAAe,IAAI,KAAK,aAAa,aAAa,YAAY;AAAA,UAC9D;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,aAAoC;AACjD,UAAM,YAAY,MAAM,KAAK,MAAM;AAAA,MAClC;AAAA,IACD;AAEA,UAAM,SAAiC,CAAC;AACxC,eAAW,YAAY,WAAW;AACjC,YAAM,YAAY,qBAAqB,SAAS,IAAI;AACpD,YAAM,UAAU,MAAM,KAAK,MAAM,MAAwB,qBAAqB,SAAS,GAAG;AAC1F,YAAM,cAAc,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI;AACvD,YAAM,OAAO,MAAM,KAAK,MAAM,MAA+B,iBAAiB,SAAS,EAAE;AAEzF,aAAO,KAAK;AAAA,QACX,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,MACD,CAAC;AAAA,IACF;AAEA,WAAO,EAAE,OAAO;AAAA,EACjB;AACD;AAEA,SAAS,qBAAqB,YAA4B;AACzD,MAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACxC,UAAM,IAAI,iBAAiB,0BAA0B,UAAU,IAAI;AAAA,MAClE,MAAM;AAAA,MACN;AAAA,IACD,CAAC;AAAA,EACF;AACA,SAAO;AACR;","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/indexeddb-adapter.ts","../../src/adapters/indexeddb-persistence-scheduler.ts","../../src/adapters/sqlite-wasm-persistence.ts"],"sourcesContent":["import type { KoraEventEmitter, SchemaDefinition } from '@korajs/core'\nimport { quoteIdent } from '@korajs/core'\nimport { PersistenceError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { IndexedDbPersistenceScheduler } from './indexeddb-persistence-scheduler'\nimport { SqliteWasmAdapter } from './sqlite-wasm-adapter'\nimport type { WorkerBridge } from './sqlite-wasm-channel'\nimport {\n\tisIndexedDbQuotaError,\n\tloadDumpFromIndexedDB,\n\tloadFromIndexedDB,\n\tsaveDumpToIndexedDB,\n\tsaveToIndexedDB,\n} from './sqlite-wasm-persistence'\n\ninterface DatabaseDump {\n\ttables: Array<{\n\t\tname: string\n\t\tcolumns: string[]\n\t\trows: Array<Record<string, unknown>>\n\t}>\n}\n\n/**\n * Options for creating an IndexedDbAdapter.\n */\nexport interface IndexedDbAdapterOptions {\n\t/**\n\t * Database name used as the IndexedDB key for persistence.\n\t * Defaults to 'kora-db'.\n\t */\n\tdbName?: string\n\n\t/**\n\t * Injected WorkerBridge for testing. If omitted, a WebWorkerBridge is created\n\t * in browser environments.\n\t */\n\tbridge?: WorkerBridge\n\n\t/**\n\t * URL to the sqlite-wasm-worker script. Required in browsers if no bridge is provided.\n\t */\n\tworkerUrl?: string | URL\n\n\t/** Timeout for worker / follower RPC responses. Defaults to 30000ms. */\n\tworkerResponseTimeoutMs?: number\n\n\t/**\n\t * Debounce interval (ms) before writing snapshots to IndexedDB. Defaults to 500.\n\t */\n\tpersistenceDebounceMs?: number\n\n\t/**\n\t * When set, persistence failures and quota errors are emitted on this emitter.\n\t */\n\temitter?: KoraEventEmitter\n}\n\n/**\n * IndexedDB-backed adapter that uses SQLite WASM in-memory and serializes\n * the entire database to IndexedDB after mutations (coalesced/debounced).\n *\n * This is the fallback adapter for browsers where OPFS is not available.\n * It provides the same SQL interface as SqliteWasmAdapter, but persists by\n * serializing the full SQLite database to a single IndexedDB blob.\n *\n * @example\n * ```typescript\n * const adapter = new IndexedDbAdapter({ workerUrl: '/sqlite-wasm-worker.js' })\n * ```\n */\nexport class IndexedDbAdapter implements StorageAdapter {\n\tprivate inner: SqliteWasmAdapter\n\tprivate readonly dbName: string\n\tprivate readonly emitter: KoraEventEmitter | undefined\n\tprivate readonly scheduler: IndexedDbPersistenceScheduler\n\n\tconstructor(options: IndexedDbAdapterOptions = {}) {\n\t\tthis.dbName = options.dbName ?? 'kora-db'\n\t\tthis.emitter = options.emitter\n\t\tthis.inner = new SqliteWasmAdapter({\n\t\t\tbridge: options.bridge,\n\t\t\tworkerUrl: options.workerUrl,\n\t\t\tdbName: this.dbName,\n\t\t\tworkerResponseTimeoutMs: options.workerResponseTimeoutMs,\n\t\t})\n\t\tthis.scheduler = new IndexedDbPersistenceScheduler({\n\t\t\tdebounceMs: options.persistenceDebounceMs,\n\t\t\tflush: () => this.writeSnapshot(),\n\t\t\tonError: (error) => this.handlePersistenceError(error),\n\t\t})\n\t}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\tawait this.inner.open(schema)\n\n\t\tconst persisted = await loadFromIndexedDB(this.dbName)\n\t\tif (!persisted) return\n\n\t\ttry {\n\t\t\tawait this.inner.importDatabase(persisted)\n\t\t} catch {\n\t\t\tawait this.restoreFromDumpFallback()\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tawait this.scheduler.flushNow()\n\t\tthis.scheduler.dispose()\n\t\tawait this.inner.close()\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tawait this.inner.execute(sql, params)\n\t\tthis.scheduler.schedule()\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\treturn this.inner.query<T>(sql, params)\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tawait this.inner.transaction(fn)\n\t\tthis.scheduler.schedule()\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tawait this.inner.migrate(from, to, migration)\n\t\tthis.scheduler.schedule()\n\t}\n\n\t/**\n\t * Force an immediate snapshot write to IndexedDB (skips debounce).\n\t * Useful before tab unload or in tests.\n\t */\n\tasync flushPersistence(): Promise<void> {\n\t\tawait this.scheduler.flushNow()\n\t}\n\n\tprivate async writeSnapshot(): Promise<void> {\n\t\tconst data = await this.inner.exportDatabase()\n\t\tawait saveToIndexedDB(this.dbName, data)\n\t\tconst dump = await this.exportDump()\n\t\tawait saveDumpToIndexedDB(this.dbName, dump)\n\t}\n\n\tprivate handlePersistenceError(error: unknown): void {\n\t\tconst message = error instanceof Error ? error.message : 'IndexedDB persistence failed'\n\t\tconst code = error instanceof PersistenceError ? error.code : 'PERSISTENCE_FAILED'\n\t\tconst quotaExceeded = isIndexedDbQuotaError(error)\n\n\t\tif (quotaExceeded) {\n\t\t\tthis.emitter?.emit({\n\t\t\t\ttype: 'store:quota-exceeded',\n\t\t\t\tdbName: this.dbName,\n\t\t\t\tmessage,\n\t\t\t})\n\t\t}\n\n\t\tthis.emitter?.emit({\n\t\t\ttype: 'store:persistence-error',\n\t\t\tdbName: this.dbName,\n\t\t\tmessage,\n\t\t\tcode: quotaExceeded ? 'QUOTA_EXCEEDED' : code,\n\t\t})\n\t}\n\n\tprivate async restoreFromDumpFallback(): Promise<void> {\n\t\tconst dump = await loadDumpFromIndexedDB<DatabaseDump>(this.dbName)\n\t\tif (!dump) return\n\n\t\tfor (const table of dump.tables) {\n\t\t\tconst name = ensureSafeIdentifier(table.name)\n\t\t\tawait this.inner.execute(`DELETE FROM ${quoteIdent(name)}`)\n\n\t\t\tif (table.rows.length === 0) continue\n\n\t\t\tfor (const row of table.rows) {\n\t\t\t\tconst columns = table.columns.filter((column) =>\n\t\t\t\t\tObject.prototype.hasOwnProperty.call(row, column),\n\t\t\t\t)\n\t\t\t\tif (columns.length === 0) continue\n\n\t\t\t\tconst placeholders = columns.map(() => '?').join(', ')\n\t\t\t\tconst quotedColumns = columns\n\t\t\t\t\t.map((column) => quoteIdent(ensureSafeIdentifier(column)))\n\t\t\t\t\t.join(', ')\n\t\t\t\tconst values = columns.map((column) => row[column])\n\n\t\t\t\tawait this.inner.execute(\n\t\t\t\t\t`INSERT INTO ${quoteIdent(name)} (${quotedColumns}) VALUES (${placeholders})`,\n\t\t\t\t\tvalues,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async exportDump(): Promise<DatabaseDump> {\n\t\tconst tableRows = await this.inner.query<{ name: string }>(\n\t\t\t\"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n\t\t)\n\n\t\tconst tables: DatabaseDump['tables'] = []\n\t\tfor (const tableRow of tableRows) {\n\t\t\tconst tableName = ensureSafeIdentifier(tableRow.name)\n\t\t\tconst columns = await this.inner.query<{ name: string }>(\n\t\t\t\t`PRAGMA table_info(${quoteIdent(tableName)})`,\n\t\t\t)\n\t\t\tconst columnNames = columns.map((column) => column.name)\n\t\t\tconst rows = await this.inner.query<Record<string, unknown>>(\n\t\t\t\t`SELECT * FROM ${quoteIdent(tableName)}`,\n\t\t\t)\n\n\t\t\ttables.push({\n\t\t\t\tname: tableName,\n\t\t\t\tcolumns: columnNames,\n\t\t\t\trows,\n\t\t\t})\n\t\t}\n\n\t\treturn { tables }\n\t}\n}\n\nfunction ensureSafeIdentifier(identifier: string): string {\n\tif (!/^[a-zA-Z0-9_]+$/.test(identifier)) {\n\t\tthrow new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {\n\t\t\tcode: 'UNSAFE_IDENTIFIER',\n\t\t\tidentifier,\n\t\t})\n\t}\n\treturn identifier\n}\n","/// <reference lib=\"dom\" />\n\nconst DEFAULT_DEBOUNCE_MS = 500\n\nexport interface IndexedDbPersistenceSchedulerOptions {\n\t/** Debounce interval before writing a snapshot. Defaults to 500ms. */\n\tdebounceMs?: number\n\t/** Persist the in-memory database to IndexedDB. */\n\tflush: () => Promise<void>\n\t/** Called when persistence fails (mutation already committed in memory). */\n\tonError?: (error: unknown) => void\n}\n\n/**\n * Coalesces IndexedDB snapshot writes: debounces rapid mutations and flushes\n * immediately on tab hide (`visibilitychange`) or explicit {@link flushNow}.\n */\nexport class IndexedDbPersistenceScheduler {\n\tprivate readonly debounceMs: number\n\tprivate readonly flush: () => Promise<void>\n\tprivate readonly onError: ((error: unknown) => void) | undefined\n\tprivate timer: ReturnType<typeof setTimeout> | null = null\n\tprivate inFlight: Promise<void> | null = null\n\tprivate disposed = false\n\tprivate readonly onVisibilityChange: () => void\n\n\tconstructor(options: IndexedDbPersistenceSchedulerOptions) {\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS\n\t\tthis.flush = options.flush\n\t\tthis.onError = options.onError\n\t\tthis.onVisibilityChange = () => {\n\t\t\tif (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n\t\t\t\tvoid this.flushNow()\n\t\t\t}\n\t\t}\n\t\tif (typeof document !== 'undefined') {\n\t\t\tdocument.addEventListener('visibilitychange', this.onVisibilityChange)\n\t\t}\n\t}\n\n\t/** Schedule a debounced snapshot write. */\n\tschedule(): void {\n\t\tif (this.disposed) return\n\t\tif (this.debounceMs <= 0) {\n\t\t\tvoid this.flushNow()\n\t\t\treturn\n\t\t}\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t}\n\t\tthis.timer = setTimeout(() => {\n\t\t\tthis.timer = null\n\t\t\tvoid this.flushNow()\n\t\t}, this.debounceMs)\n\t}\n\n\t/** Cancel any pending debounce and persist immediately. */\n\tasync flushNow(): Promise<void> {\n\t\tif (this.disposed) return\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tthis.timer = null\n\t\t}\n\t\tif (this.inFlight) {\n\t\t\tawait this.inFlight\n\t\t\treturn\n\t\t}\n\t\tthis.inFlight = this.runFlush()\n\t\ttry {\n\t\t\tawait this.inFlight\n\t\t} finally {\n\t\t\tthis.inFlight = null\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tthis.timer = null\n\t\t}\n\t\tif (typeof document !== 'undefined') {\n\t\t\tdocument.removeEventListener('visibilitychange', this.onVisibilityChange)\n\t\t}\n\t}\n\n\tprivate async runFlush(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.flush()\n\t\t} catch (error) {\n\t\t\tthis.onError?.(error)\n\t\t}\n\t}\n}\n","/// <reference lib=\"dom\" />\nimport { PersistenceError } from '../errors'\n\nconst IDB_DATABASE_NAME = 'kora-persistence'\nconst IDB_STORE_NAME = 'databases'\nconst IDB_VERSION = 1\n\nconst DUMP_SUFFIX = '::dump'\n\n/**\n * Returns true when an IndexedDB error indicates storage quota was exceeded.\n */\nexport function isIndexedDbQuotaError(error: unknown): boolean {\n\tif (error instanceof DOMException && error.name === 'QuotaExceededError') {\n\t\treturn true\n\t}\n\tif (error instanceof PersistenceError && error.context?.quotaExceeded === true) {\n\t\treturn true\n\t}\n\tif (error instanceof Error && /quota/i.test(error.message)) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunction rejectWithIdbError(\n\treject: (reason: unknown) => void,\n\tmessage: string,\n\tcontext: Record<string, unknown>,\n\tevent: Event,\n): void {\n\tconst domError = (event.target as IDBRequest | IDBTransaction | undefined)?.error\n\tif (domError && isIndexedDbQuotaError(domError)) {\n\t\treject(\n\t\t\tnew PersistenceError(message, {\n\t\t\t\t...context,\n\t\t\t\tquotaExceeded: true,\n\t\t\t\tcause: domError.message,\n\t\t\t}),\n\t\t)\n\t\treturn\n\t}\n\treject(new PersistenceError(message, context))\n}\n\n/**\n * Open the IndexedDB database used for SQLite persistence.\n * Creates the object store on first access.\n */\nfunction openIdb(): Promise<IDBDatabase> {\n\treturn new Promise<IDBDatabase>((resolve, reject) => {\n\t\tconst request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION)\n\t\trequest.onupgradeneeded = () => {\n\t\t\tconst db = request.result\n\t\t\tif (!db.objectStoreNames.contains(IDB_STORE_NAME)) {\n\t\t\t\tdb.createObjectStore(IDB_STORE_NAME)\n\t\t\t}\n\t\t}\n\t\trequest.onsuccess = () => resolve(request.result)\n\t\trequest.onerror = () =>\n\t\t\treject(\n\t\t\t\tnew PersistenceError(\n\t\t\t\t\t`Failed to open IndexedDB: ${request.error?.message ?? 'unknown error'}`,\n\t\t\t\t\t{ database: IDB_DATABASE_NAME },\n\t\t\t\t),\n\t\t\t)\n\t})\n}\n\n/**\n * Save a serialized SQLite database to IndexedDB.\n *\n * @param dbName - Key under which to store the data\n * @param data - Serialized database as Uint8Array\n */\nexport async function saveToIndexedDB(dbName: string, data: Uint8Array): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.put(data, dbName)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = (event) =>\n\t\t\t\trejectWithIdbError(\n\t\t\t\t\treject,\n\t\t\t\t\t`Failed to save database \"${dbName}\" to IndexedDB`,\n\t\t\t\t\t{ dbName },\n\t\t\t\t\tevent,\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Save a logical SQL dump payload to IndexedDB for import-fallback restore.\n */\nexport async function saveDumpToIndexedDB(dbName: string, dump: unknown): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.put(dump, `${dbName}${DUMP_SUFFIX}`)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = (event) =>\n\t\t\t\trejectWithIdbError(\n\t\t\t\t\treject,\n\t\t\t\t\t`Failed to save dump for database \"${dbName}\"`,\n\t\t\t\t\t{ dbName },\n\t\t\t\t\tevent,\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Load a serialized SQLite database from IndexedDB.\n *\n * @param dbName - Key under which the data was stored\n * @returns The serialized database, or null if not found\n */\nexport async function loadFromIndexedDB(dbName: string): Promise<Uint8Array | null> {\n\tconst idb = await openIdb()\n\ttry {\n\t\treturn await new Promise<Uint8Array | null>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readonly')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tconst request = store.get(dbName)\n\t\t\trequest.onsuccess = () => {\n\t\t\t\tconst result = request.result\n\t\t\t\tif (result instanceof Uint8Array) {\n\t\t\t\t\tresolve(result)\n\t\t\t\t} else if (result) {\n\t\t\t\t\t// Handle ArrayBuffer or other typed array forms\n\t\t\t\t\tresolve(new Uint8Array(result as ArrayBuffer))\n\t\t\t\t} else {\n\t\t\t\t\tresolve(null)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequest.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to load database \"${dbName}\" from IndexedDB`, { dbName }),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Load a logical SQL dump payload from IndexedDB.\n */\nexport async function loadDumpFromIndexedDB<T>(dbName: string): Promise<T | null> {\n\tconst idb = await openIdb()\n\ttry {\n\t\treturn await new Promise<T | null>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readonly')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tconst request = store.get(`${dbName}${DUMP_SUFFIX}`)\n\t\t\trequest.onsuccess = () => {\n\t\t\t\tresolve((request.result as T | undefined) ?? null)\n\t\t\t}\n\t\t\trequest.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to load dump for database \"${dbName}\" from IndexedDB`, {\n\t\t\t\t\t\tdbName,\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n\n/**\n * Delete a serialized SQLite database from IndexedDB.\n *\n * @param dbName - Key to delete\n */\nexport async function deleteFromIndexedDB(dbName: string): Promise<void> {\n\tconst idb = await openIdb()\n\ttry {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst tx = idb.transaction(IDB_STORE_NAME, 'readwrite')\n\t\t\tconst store = tx.objectStore(IDB_STORE_NAME)\n\t\t\tstore.delete(dbName)\n\t\t\tstore.delete(`${dbName}${DUMP_SUFFIX}`)\n\t\t\ttx.oncomplete = () => resolve()\n\t\t\ttx.onerror = () =>\n\t\t\t\treject(\n\t\t\t\t\tnew PersistenceError(`Failed to delete database \"${dbName}\" from IndexedDB`, { dbName }),\n\t\t\t\t)\n\t\t})\n\t} finally {\n\t\tidb.close()\n\t}\n}\n"],"mappings":";;;;;;;;;;AACA,SAAS,kBAAkB;;;ACC3B,IAAM,sBAAsB;AAerB,IAAM,gCAAN,MAAoC;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACT,QAA8C;AAAA,EAC9C,WAAiC;AAAA,EACjC,WAAW;AAAA,EACF;AAAA,EAEjB,YAAY,SAA+C;AAC1D,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ;AACvB,SAAK,qBAAqB,MAAM;AAC/B,UAAI,OAAO,aAAa,eAAe,SAAS,oBAAoB,UAAU;AAC7E,aAAK,KAAK,SAAS;AAAA,MACpB;AAAA,IACD;AACA,QAAI,OAAO,aAAa,aAAa;AACpC,eAAS,iBAAiB,oBAAoB,KAAK,kBAAkB;AAAA,IACtE;AAAA,EACD;AAAA;AAAA,EAGA,WAAiB;AAChB,QAAI,KAAK,SAAU;AACnB,QAAI,KAAK,cAAc,GAAG;AACzB,WAAK,KAAK,SAAS;AACnB;AAAA,IACD;AACA,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AAAA,IACxB;AACA,SAAK,QAAQ,WAAW,MAAM;AAC7B,WAAK,QAAQ;AACb,WAAK,KAAK,SAAS;AAAA,IACpB,GAAG,KAAK,UAAU;AAAA,EACnB;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC/B,QAAI,KAAK,SAAU;AACnB,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACd;AACA,QAAI,KAAK,UAAU;AAClB,YAAM,KAAK;AACX;AAAA,IACD;AACA,SAAK,WAAW,KAAK,SAAS;AAC9B,QAAI;AACH,YAAM,KAAK;AAAA,IACZ,UAAE;AACD,WAAK,WAAW;AAAA,IACjB;AAAA,EACD;AAAA,EAEA,UAAgB;AACf,SAAK,WAAW;AAChB,QAAI,KAAK,UAAU,MAAM;AACxB,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACd;AACA,QAAI,OAAO,aAAa,aAAa;AACpC,eAAS,oBAAoB,oBAAoB,KAAK,kBAAkB;AAAA,IACzE;AAAA,EACD;AAAA,EAEA,MAAc,WAA0B;AACvC,QAAI;AACH,YAAM,KAAK,MAAM;AAAA,IAClB,SAAS,OAAO;AACf,WAAK,UAAU,KAAK;AAAA,IACrB;AAAA,EACD;AACD;;;AC1FA,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB;AACvB,IAAM,cAAc;AAEpB,IAAM,cAAc;AAKb,SAAS,sBAAsB,OAAyB;AAC9D,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,sBAAsB;AACzE,WAAO;AAAA,EACR;AACA,MAAI,iBAAiB,oBAAoB,MAAM,SAAS,kBAAkB,MAAM;AAC/E,WAAO;AAAA,EACR;AACA,MAAI,iBAAiB,SAAS,SAAS,KAAK,MAAM,OAAO,GAAG;AAC3D,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAEA,SAAS,mBACR,QACA,SACA,SACA,OACO;AACP,QAAM,WAAY,MAAM,QAAoD;AAC5E,MAAI,YAAY,sBAAsB,QAAQ,GAAG;AAChD;AAAA,MACC,IAAI,iBAAiB,SAAS;AAAA,QAC7B,GAAG;AAAA,QACH,eAAe;AAAA,QACf,OAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA;AAAA,EACD;AACA,SAAO,IAAI,iBAAiB,SAAS,OAAO,CAAC;AAC9C;AAMA,SAAS,UAAgC;AACxC,SAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACpD,UAAM,UAAU,UAAU,KAAK,mBAAmB,WAAW;AAC7D,YAAQ,kBAAkB,MAAM;AAC/B,YAAM,KAAK,QAAQ;AACnB,UAAI,CAAC,GAAG,iBAAiB,SAAS,cAAc,GAAG;AAClD,WAAG,kBAAkB,cAAc;AAAA,MACpC;AAAA,IACD;AACA,YAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,YAAQ,UAAU,MACjB;AAAA,MACC,IAAI;AAAA,QACH,6BAA6B,QAAQ,OAAO,WAAW,eAAe;AAAA,QACtE,EAAE,UAAU,kBAAkB;AAAA,MAC/B;AAAA,IACD;AAAA,EACF,CAAC;AACF;AAQA,eAAsB,gBAAgB,QAAgB,MAAiC;AACtF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,KAAK,IAAI,YAAY,gBAAgB,WAAW;AACtD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,IAAI,MAAM,MAAM;AACtB,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,CAAC,UACb;AAAA,QACC;AAAA,QACA,4BAA4B,MAAM;AAAA,QAClC,EAAE,OAAO;AAAA,QACT;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAKA,eAAsB,oBAAoB,QAAgB,MAA8B;AACvF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,KAAK,IAAI,YAAY,gBAAgB,WAAW;AACtD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,IAAI,MAAM,GAAG,MAAM,GAAG,WAAW,EAAE;AACzC,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,CAAC,UACb;AAAA,QACC;AAAA,QACA,qCAAqC,MAAM;AAAA,QAC3C,EAAE,OAAO;AAAA,QACT;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAQA,eAAsB,kBAAkB,QAA4C;AACnF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,WAAO,MAAM,IAAI,QAA2B,CAAC,SAAS,WAAW;AAChE,YAAM,KAAK,IAAI,YAAY,gBAAgB,UAAU;AACrD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,UAAU,MAAM,IAAI,MAAM;AAChC,cAAQ,YAAY,MAAM;AACzB,cAAM,SAAS,QAAQ;AACvB,YAAI,kBAAkB,YAAY;AACjC,kBAAQ,MAAM;AAAA,QACf,WAAW,QAAQ;AAElB,kBAAQ,IAAI,WAAW,MAAqB,CAAC;AAAA,QAC9C,OAAO;AACN,kBAAQ,IAAI;AAAA,QACb;AAAA,MACD;AACA,cAAQ,UAAU,MACjB;AAAA,QACC,IAAI,iBAAiB,4BAA4B,MAAM,oBAAoB,EAAE,OAAO,CAAC;AAAA,MACtF;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;AAKA,eAAsB,sBAAyB,QAAmC;AACjF,QAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI;AACH,WAAO,MAAM,IAAI,QAAkB,CAAC,SAAS,WAAW;AACvD,YAAM,KAAK,IAAI,YAAY,gBAAgB,UAAU;AACrD,YAAM,QAAQ,GAAG,YAAY,cAAc;AAC3C,YAAM,UAAU,MAAM,IAAI,GAAG,MAAM,GAAG,WAAW,EAAE;AACnD,cAAQ,YAAY,MAAM;AACzB,gBAAS,QAAQ,UAA4B,IAAI;AAAA,MAClD;AACA,cAAQ,UAAU,MACjB;AAAA,QACC,IAAI,iBAAiB,qCAAqC,MAAM,oBAAoB;AAAA,UACnF;AAAA,QACD,CAAC;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,MAAM;AAAA,EACX;AACD;;;AF1GO,IAAM,mBAAN,MAAiD;AAAA,EAC/C;AAAA,EACS;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAAmC,CAAC,GAAG;AAClD,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,UAAU,QAAQ;AACvB,SAAK,QAAQ,IAAI,kBAAkB;AAAA,MAClC,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,yBAAyB,QAAQ;AAAA,IAClC,CAAC;AACD,SAAK,YAAY,IAAI,8BAA8B;AAAA,MAClD,YAAY,QAAQ;AAAA,MACpB,OAAO,MAAM,KAAK,cAAc;AAAA,MAChC,SAAS,CAAC,UAAU,KAAK,uBAAuB,KAAK;AAAA,IACtD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,QAAyC;AACnD,UAAM,KAAK,MAAM,KAAK,MAAM;AAE5B,UAAM,YAAY,MAAM,kBAAkB,KAAK,MAAM;AACrD,QAAI,CAAC,UAAW;AAEhB,QAAI;AACH,YAAM,KAAK,MAAM,eAAe,SAAS;AAAA,IAC1C,QAAQ;AACP,YAAM,KAAK,wBAAwB;AAAA,IACpC;AAAA,EACD;AAAA,EAEA,MAAM,QAAuB;AAC5B,UAAM,KAAK,UAAU,SAAS;AAC9B,SAAK,UAAU,QAAQ;AACvB,UAAM,KAAK,MAAM,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,QAAQ,KAAa,QAAmC;AAC7D,UAAM,KAAK,MAAM,QAAQ,KAAK,MAAM;AACpC,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA,EAEA,MAAM,MAAS,KAAa,QAAkC;AAC7D,WAAO,KAAK,MAAM,MAAS,KAAK,MAAM;AAAA,EACvC;AAAA,EAEA,MAAM,YAAY,IAAuD;AACxE,UAAM,KAAK,MAAM,YAAY,EAAE;AAC/B,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA,EAEA,MAAM,QAAQ,MAAc,IAAY,WAAyC;AAChF,UAAM,KAAK,MAAM,QAAQ,MAAM,IAAI,SAAS;AAC5C,SAAK,UAAU,SAAS;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,mBAAkC;AACvC,UAAM,KAAK,UAAU,SAAS;AAAA,EAC/B;AAAA,EAEA,MAAc,gBAA+B;AAC5C,UAAM,OAAO,MAAM,KAAK,MAAM,eAAe;AAC7C,UAAM,gBAAgB,KAAK,QAAQ,IAAI;AACvC,UAAM,OAAO,MAAM,KAAK,WAAW;AACnC,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAAA,EAC5C;AAAA,EAEQ,uBAAuB,OAAsB;AACpD,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;AAC9D,UAAM,gBAAgB,sBAAsB,KAAK;AAEjD,QAAI,eAAe;AAClB,WAAK,SAAS,KAAK;AAAA,QAClB,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,QACb;AAAA,MACD,CAAC;AAAA,IACF;AAEA,SAAK,SAAS,KAAK;AAAA,MAClB,MAAM;AAAA,MACN,QAAQ,KAAK;AAAA,MACb;AAAA,MACA,MAAM,gBAAgB,mBAAmB;AAAA,IAC1C,CAAC;AAAA,EACF;AAAA,EAEA,MAAc,0BAAyC;AACtD,UAAM,OAAO,MAAM,sBAAoC,KAAK,MAAM;AAClE,QAAI,CAAC,KAAM;AAEX,eAAW,SAAS,KAAK,QAAQ;AAChC,YAAM,OAAO,qBAAqB,MAAM,IAAI;AAC5C,YAAM,KAAK,MAAM,QAAQ,eAAe,WAAW,IAAI,CAAC,EAAE;AAE1D,UAAI,MAAM,KAAK,WAAW,EAAG;AAE7B,iBAAW,OAAO,MAAM,MAAM;AAC7B,cAAM,UAAU,MAAM,QAAQ;AAAA,UAAO,CAAC,WACrC,OAAO,UAAU,eAAe,KAAK,KAAK,MAAM;AAAA,QACjD;AACA,YAAI,QAAQ,WAAW,EAAG;AAE1B,cAAM,eAAe,QAAQ,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AACrD,cAAM,gBAAgB,QACpB,IAAI,CAAC,WAAW,WAAW,qBAAqB,MAAM,CAAC,CAAC,EACxD,KAAK,IAAI;AACX,cAAM,SAAS,QAAQ,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC;AAElD,cAAM,KAAK,MAAM;AAAA,UAChB,eAAe,WAAW,IAAI,CAAC,KAAK,aAAa,aAAa,YAAY;AAAA,UAC1E;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,aAAoC;AACjD,UAAM,YAAY,MAAM,KAAK,MAAM;AAAA,MAClC;AAAA,IACD;AAEA,UAAM,SAAiC,CAAC;AACxC,eAAW,YAAY,WAAW;AACjC,YAAM,YAAY,qBAAqB,SAAS,IAAI;AACpD,YAAM,UAAU,MAAM,KAAK,MAAM;AAAA,QAChC,qBAAqB,WAAW,SAAS,CAAC;AAAA,MAC3C;AACA,YAAM,cAAc,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI;AACvD,YAAM,OAAO,MAAM,KAAK,MAAM;AAAA,QAC7B,iBAAiB,WAAW,SAAS,CAAC;AAAA,MACvC;AAEA,aAAO,KAAK;AAAA,QACX,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,MACD,CAAC;AAAA,IACF;AAEA,WAAO,EAAE,OAAO;AAAA,EACjB;AACD;AAEA,SAAS,qBAAqB,YAA4B;AACzD,MAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACxC,UAAM,IAAI,iBAAiB,0BAA0B,UAAU,IAAI;AAAA,MAClE,MAAM;AAAA,MACN;AAAA,IACD,CAAC;AAAA,EACF;AACA,SAAO;AACR;","names":[]}
@@ -99,6 +99,7 @@ async function handleOpen(id, ddlStatements, dbName) {
99
99
  );
100
100
  sqlite3Api = sqlite3;
101
101
  let useOpfs = false;
102
+ let fallbackReason;
102
103
  if (sqlite3.installOpfsSAHPoolVfs) {
103
104
  try {
104
105
  const pool = await withTimeout(
@@ -108,9 +109,14 @@ async function handleOpen(id, ddlStatements, dbName) {
108
109
  );
109
110
  db = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? "kora-db"));
110
111
  useOpfs = true;
111
- } catch {
112
- console.warn("[kora] OPFS unavailable, falling back to in-memory SQLite");
112
+ } catch (error) {
113
+ fallbackReason = classifyOpfsFailure(error);
114
+ console.warn(
115
+ `[kora] OPFS unavailable (${fallbackReason}), falling back to in-memory SQLite`
116
+ );
113
117
  }
118
+ } else {
119
+ fallbackReason = "unsupported";
114
120
  }
115
121
  if (!useOpfs) {
116
122
  db = new sqlite3.oo1.DB({ filename: ":memory:" });
@@ -131,7 +137,11 @@ async function handleOpen(id, ddlStatements, dbName) {
131
137
  db?.exec({ sql });
132
138
  }
133
139
  }
134
- sendResponse({ id, type: "success" });
140
+ sendResponse({
141
+ id,
142
+ type: "success",
143
+ data: { persistent: useOpfs, ...fallbackReason ? { fallbackReason } : {} }
144
+ });
135
145
  } catch (error) {
136
146
  sendResponse({
137
147
  id,
@@ -141,6 +151,18 @@ async function handleOpen(id, ddlStatements, dbName) {
141
151
  });
142
152
  }
143
153
  }
154
+ function classifyOpfsFailure(error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ if (/timed out|timeout/i.test(message)) {
157
+ return "timeout";
158
+ }
159
+ if (/lock|already|in use|in-use|NoModificationAllowed|acquire|SyncAccessHandle|being created/i.test(
160
+ message
161
+ )) {
162
+ return "lock-conflict";
163
+ }
164
+ return "unsupported";
165
+ }
144
166
  function handleClose(id) {
145
167
  if (db) {
146
168
  db.close();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/sqlite-wasm-worker.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * Web Worker script for running SQLite WASM.\n *\n * This file is intended to run inside a Web Worker in browsers.\n * It loads @sqlite.org/sqlite-wasm, initializes SQLite with OPFS persistence\n * (falling back to in-memory if unavailable), and processes messages from\n * the main thread via the WorkerRequest/WorkerResponse protocol.\n *\n * This script cannot be tested in Node.js — it is validated in E2E browser tests.\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\ninterface SqliteDb {\n\texec(opts: {\n\t\tsql: string\n\t\tbind?: unknown[]\n\t\treturnValue?: string\n\t\trowMode?: string\n\t\tcallback?: (row: Record<string, unknown>) => void\n\t}): void\n\tclose(): void\n\tdeserialize?: (data: Uint8Array) => void\n}\n\ndeclare const self: DedicatedWorkerGlobalScope\n\nlet db: SqliteDb | null = null\nlet sqlite3Api: unknown = null\n\nfunction sendResponse(response: WorkerResponse): void {\n\tself.postMessage(response)\n}\n\nfunction handleExecute(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tdb.exec({ sql, bind: params })\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'EXEC_ERROR' })\n\t}\n}\n\nfunction handleQuery(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tconst rows: Record<string, unknown>[] = []\n\t\tdb.exec({\n\t\t\tsql,\n\t\t\tbind: params,\n\t\t\trowMode: 'object',\n\t\t\tcallback: (row: Record<string, unknown>) => {\n\t\t\t\trows.push({ ...row })\n\t\t\t},\n\t\t})\n\t\tsendResponse({ id, type: 'success', data: rows })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'QUERY_ERROR' })\n\t}\n}\n\nfunction opfsDatabaseFilename(dbName: string): string {\n\tconst base = dbName.replace(/[^a-zA-Z0-9._-]/g, '_')\n\treturn base.endsWith('.db') ? base : `${base}.db`\n}\n\n/** Headless browsers and some profiles hang on OPFS VFS install; fall back to memory. */\nconst OPFS_INIT_TIMEOUT_MS = 10_000\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tpromise,\n\t\t\tnew Promise<T>((_, reject) => {\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t)\n\t\t\t}),\n\t\t])\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer)\n\t}\n}\n\nasync function handleOpen(id: number, ddlStatements: string[], dbName?: string): Promise<void> {\n\ttry {\n\t\tconst sqlite3InitModule = (await import('@sqlite.org/sqlite-wasm')).default\n\n\t\t// In production builds, Vite hashes asset filenames (e.g. sqlite3-[hash].wasm).\n\t\t// The sqlite3 module's default locateFile resolves the unhashed name, causing a 404.\n\t\t// Templates set __KORA_SQLITE_WASM_URL via `import wasmUrl from '...sqlite3.wasm?url'`\n\t\t// so we can override locateFile with the correct hashed URL.\n\t\tconst wasmUrl = (globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL as\n\t\t\t| string\n\t\t\t| undefined\n\t\tconst initOptions = wasmUrl\n\t\t\t? {\n\t\t\t\t\tlocateFile: (file: string) => {\n\t\t\t\t\t\tif (file.endsWith('.wasm')) return wasmUrl\n\t\t\t\t\t\treturn file\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: undefined\n\n\t\t// sqlite3InitModule accepts Emscripten Module overrides but the types don't reflect this\n\t\tconst initFn = sqlite3InitModule as unknown as (\n\t\t\topts?: Record<string, unknown>,\n\t\t) => Promise<unknown>\n\t\tconst sqlite3 = (await withTimeout(\n\t\t\tinitFn(initOptions),\n\t\t\t60_000,\n\t\t\t'SQLite3 module init',\n\t\t)) as Awaited<ReturnType<typeof sqlite3InitModule>>\n\t\tsqlite3Api = sqlite3\n\n\t\t// Try OPFS persistence first\n\t\tlet useOpfs = false\n\t\tif (sqlite3.installOpfsSAHPoolVfs) {\n\t\t\ttry {\n\t\t\t\tconst pool = await withTimeout(\n\t\t\t\t\tsqlite3.installOpfsSAHPoolVfs({ name: 'kora-opfs' }),\n\t\t\t\t\tOPFS_INIT_TIMEOUT_MS,\n\t\t\t\t\t'OPFS VFS install',\n\t\t\t\t)\n\t\t\t\tdb = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? 'kora-db'))\n\t\t\t\tuseOpfs = true\n\t\t\t} catch {\n\t\t\t\t// OPFS unavailable or too slow (common in headless CI), fall back to in-memory\n\t\t\t\tconsole.warn('[kora] OPFS unavailable, falling back to in-memory SQLite')\n\t\t\t}\n\t\t}\n\n\t\tif (!useOpfs) {\n\t\t\tdb = new sqlite3.oo1.DB({ filename: ':memory:' })\n\t\t}\n\n\t\t// Set pragmas\n\t\tdb?.exec({ sql: 'PRAGMA journal_mode = WAL' })\n\t\tdb?.exec({ sql: 'PRAGMA foreign_keys = ON' })\n\n\t\t// Execute DDL statements\n\t\tfor (const sql of ddlStatements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tdb?.exec({ sql: sql.replace('--kora:safe-alter\\n', '') })\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\tif (!msg.includes('duplicate column name')) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdb?.exec({ sql })\n\t\t\t}\n\t\t}\n\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'INIT_ERROR',\n\t\t})\n\t}\n}\n\nfunction handleClose(id: number): void {\n\tif (db) {\n\t\tdb.close()\n\t\tdb = null\n\t}\n\tsendResponse({ id, type: 'success' })\n}\n\nfunction handleImport(id: number, data: Uint8Array): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\n\tconst dbWithDeserialize = db as SqliteDb & { deserialize?: (bytes: Uint8Array) => void }\n\tif (typeof dbWithDeserialize.deserialize === 'function') {\n\t\ttry {\n\t\t\tdbWithDeserialize.deserialize(data)\n\t\t\tsendResponse({ id, type: 'success' })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'IMPORT_ERROR' })\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst sqlite3 = sqlite3Api as {\n\t\too1?: { DB?: new (...args: unknown[]) => SqliteDb }\n\t\tcapi?: { sqlite3_deserialize?: unknown }\n\t} | null\n\n\tif (!sqlite3 || typeof sqlite3.capi?.sqlite3_deserialize === 'undefined') {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: 'Import not supported in this SQLite WASM runtime',\n\t\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t\t})\n\t\treturn\n\t}\n\n\tsendResponse({\n\t\tid,\n\t\ttype: 'error',\n\t\tmessage:\n\t\t\t'Import requires runtime-specific deserialize wiring and is unavailable in this worker build',\n\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t})\n}\n\nfunction handleMessage(request: WorkerRequest): void {\n\ttry {\n\t\tswitch (request.type) {\n\t\t\tcase 'open':\n\t\t\t\t// open is async due to WASM loading\n\t\t\t\thandleOpen(request.id, request.ddlStatements, request.dbName)\n\t\t\t\treturn\n\t\t\tcase 'close':\n\t\t\t\thandleClose(request.id)\n\t\t\t\treturn\n\t\t\tcase 'execute':\n\t\t\t\thandleExecute(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'query':\n\t\t\t\thandleQuery(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'begin':\n\t\t\t\thandleExecute(request.id, 'BEGIN')\n\t\t\t\treturn\n\t\t\tcase 'commit':\n\t\t\t\thandleExecute(request.id, 'COMMIT')\n\t\t\t\treturn\n\t\t\tcase 'rollback':\n\t\t\t\thandleExecute(request.id, 'ROLLBACK')\n\t\t\t\treturn\n\t\t\tcase 'migrate':\n\t\t\t\tif (!db) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: 'Database is not open',\n\t\t\t\t\t\tcode: 'DB_NOT_OPEN',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tfor (const sql of request.statements) {\n\t\t\t\t\t\tdb.exec({ sql })\n\t\t\t\t\t}\n\t\t\t\t\tsendResponse({ id: request.id, type: 'success' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: (error as Error).message,\n\t\t\t\t\t\tcode: 'MIGRATE_ERROR',\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase 'export':\n\t\t\t\t// Export is not trivially supported via the oo1 API in the browser.\n\t\t\t\t// In a real implementation, we'd use the C API's sqlite3_serialize.\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Export not yet supported in browser worker',\n\t\t\t\t\tcode: 'EXPORT_NOT_SUPPORTED',\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase 'import':\n\t\t\t\thandleImport(request.id, request.data)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: (request as WorkerRequest).id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Unknown request type',\n\t\t\t\t\tcode: 'UNKNOWN_REQUEST',\n\t\t\t\t})\n\t\t}\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid: request.id,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'WORKER_ERROR',\n\t\t})\n\t}\n}\n\n// Listen for messages from the main thread\nself.onmessage = (event: MessageEvent<WorkerRequest>) => {\n\thandleMessage(event.data)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/sqlite-wasm-worker.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * Web Worker script for running SQLite WASM.\n *\n * This file is intended to run inside a Web Worker in browsers.\n * It loads @sqlite.org/sqlite-wasm, initializes SQLite with OPFS persistence\n * (falling back to in-memory if unavailable), and processes messages from\n * the main thread via the WorkerRequest/WorkerResponse protocol.\n *\n * This script cannot be tested in Node.js — it is validated in E2E browser tests.\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\ninterface SqliteDb {\n\texec(opts: {\n\t\tsql: string\n\t\tbind?: unknown[]\n\t\treturnValue?: string\n\t\trowMode?: string\n\t\tcallback?: (row: Record<string, unknown>) => void\n\t}): void\n\tclose(): void\n\tdeserialize?: (data: Uint8Array) => void\n}\n\ndeclare const self: DedicatedWorkerGlobalScope\n\nlet db: SqliteDb | null = null\nlet sqlite3Api: unknown = null\n\nfunction sendResponse(response: WorkerResponse): void {\n\tself.postMessage(response)\n}\n\nfunction handleExecute(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tdb.exec({ sql, bind: params })\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'EXEC_ERROR' })\n\t}\n}\n\nfunction handleQuery(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tconst rows: Record<string, unknown>[] = []\n\t\tdb.exec({\n\t\t\tsql,\n\t\t\tbind: params,\n\t\t\trowMode: 'object',\n\t\t\tcallback: (row: Record<string, unknown>) => {\n\t\t\t\trows.push({ ...row })\n\t\t\t},\n\t\t})\n\t\tsendResponse({ id, type: 'success', data: rows })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'QUERY_ERROR' })\n\t}\n}\n\nfunction opfsDatabaseFilename(dbName: string): string {\n\tconst base = dbName.replace(/[^a-zA-Z0-9._-]/g, '_')\n\treturn base.endsWith('.db') ? base : `${base}.db`\n}\n\n/** Headless browsers and some profiles hang on OPFS VFS install; fall back to memory. */\nconst OPFS_INIT_TIMEOUT_MS = 10_000\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tpromise,\n\t\t\tnew Promise<T>((_, reject) => {\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t)\n\t\t\t}),\n\t\t])\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer)\n\t}\n}\n\nasync function handleOpen(id: number, ddlStatements: string[], dbName?: string): Promise<void> {\n\ttry {\n\t\tconst sqlite3InitModule = (await import('@sqlite.org/sqlite-wasm')).default\n\n\t\t// In production builds, Vite hashes asset filenames (e.g. sqlite3-[hash].wasm).\n\t\t// The sqlite3 module's default locateFile resolves the unhashed name, causing a 404.\n\t\t// Templates set __KORA_SQLITE_WASM_URL via `import wasmUrl from '...sqlite3.wasm?url'`\n\t\t// so we can override locateFile with the correct hashed URL.\n\t\tconst wasmUrl = (globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL as\n\t\t\t| string\n\t\t\t| undefined\n\t\tconst initOptions = wasmUrl\n\t\t\t? {\n\t\t\t\t\tlocateFile: (file: string) => {\n\t\t\t\t\t\tif (file.endsWith('.wasm')) return wasmUrl\n\t\t\t\t\t\treturn file\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: undefined\n\n\t\t// sqlite3InitModule accepts Emscripten Module overrides but the types don't reflect this\n\t\tconst initFn = sqlite3InitModule as unknown as (\n\t\t\topts?: Record<string, unknown>,\n\t\t) => Promise<unknown>\n\t\tconst sqlite3 = (await withTimeout(\n\t\t\tinitFn(initOptions),\n\t\t\t60_000,\n\t\t\t'SQLite3 module init',\n\t\t)) as Awaited<ReturnType<typeof sqlite3InitModule>>\n\t\tsqlite3Api = sqlite3\n\n\t\t// Try OPFS persistence first\n\t\tlet useOpfs = false\n\t\tlet fallbackReason: 'lock-conflict' | 'timeout' | 'unsupported' | undefined\n\t\tif (sqlite3.installOpfsSAHPoolVfs) {\n\t\t\ttry {\n\t\t\t\tconst pool = await withTimeout(\n\t\t\t\t\tsqlite3.installOpfsSAHPoolVfs({ name: 'kora-opfs' }),\n\t\t\t\t\tOPFS_INIT_TIMEOUT_MS,\n\t\t\t\t\t'OPFS VFS install',\n\t\t\t\t)\n\t\t\t\tdb = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? 'kora-db'))\n\t\t\t\tuseOpfs = true\n\t\t\t} catch (error) {\n\t\t\t\t// OPFS unavailable. Classify WHY so the main thread can surface a clear\n\t\t\t\t// diagnostic instead of a silent, non-persistent in-memory fallback that\n\t\t\t\t// loses the user's data on reload.\n\t\t\t\tfallbackReason = classifyOpfsFailure(error)\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[kora] OPFS unavailable (${fallbackReason}), falling back to in-memory SQLite`,\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tfallbackReason = 'unsupported'\n\t\t}\n\n\t\tif (!useOpfs) {\n\t\t\tdb = new sqlite3.oo1.DB({ filename: ':memory:' })\n\t\t}\n\n\t\t// Set pragmas\n\t\tdb?.exec({ sql: 'PRAGMA journal_mode = WAL' })\n\t\tdb?.exec({ sql: 'PRAGMA foreign_keys = ON' })\n\n\t\t// Execute DDL statements\n\t\tfor (const sql of ddlStatements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tdb?.exec({ sql: sql.replace('--kora:safe-alter\\n', '') })\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\tif (!msg.includes('duplicate column name')) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdb?.exec({ sql })\n\t\t\t}\n\t\t}\n\n\t\t// Report the storage mode so the adapter can emit `store:opfs-unavailable`\n\t\t// when persistence silently degraded to in-memory.\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'success',\n\t\t\tdata: { persistent: useOpfs, ...(fallbackReason ? { fallbackReason } : {}) },\n\t\t})\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'INIT_ERROR',\n\t\t})\n\t}\n}\n\n/**\n * Classify why the OPFS SyncAccessHandle pool could not be installed, so the\n * fallback to in-memory is explainable rather than silent.\n *\n * A `lock-conflict` is the important case: another runtime on the same origin\n * (another tab or a logically separate app that reused the database name) already\n * holds the pool's access handles, so this runtime cannot persist.\n */\nfunction classifyOpfsFailure(error: unknown): 'lock-conflict' | 'timeout' | 'unsupported' {\n\tconst message = error instanceof Error ? error.message : String(error)\n\tif (/timed out|timeout/i.test(message)) {\n\t\treturn 'timeout'\n\t}\n\tif (\n\t\t/lock|already|in use|in-use|NoModificationAllowed|acquire|SyncAccessHandle|being created/i.test(\n\t\t\tmessage,\n\t\t)\n\t) {\n\t\treturn 'lock-conflict'\n\t}\n\treturn 'unsupported'\n}\n\nfunction handleClose(id: number): void {\n\tif (db) {\n\t\tdb.close()\n\t\tdb = null\n\t}\n\tsendResponse({ id, type: 'success' })\n}\n\nfunction handleImport(id: number, data: Uint8Array): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\n\tconst dbWithDeserialize = db as SqliteDb & { deserialize?: (bytes: Uint8Array) => void }\n\tif (typeof dbWithDeserialize.deserialize === 'function') {\n\t\ttry {\n\t\t\tdbWithDeserialize.deserialize(data)\n\t\t\tsendResponse({ id, type: 'success' })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'IMPORT_ERROR' })\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst sqlite3 = sqlite3Api as {\n\t\too1?: { DB?: new (...args: unknown[]) => SqliteDb }\n\t\tcapi?: { sqlite3_deserialize?: unknown }\n\t} | null\n\n\tif (!sqlite3 || typeof sqlite3.capi?.sqlite3_deserialize === 'undefined') {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: 'Import not supported in this SQLite WASM runtime',\n\t\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t\t})\n\t\treturn\n\t}\n\n\tsendResponse({\n\t\tid,\n\t\ttype: 'error',\n\t\tmessage:\n\t\t\t'Import requires runtime-specific deserialize wiring and is unavailable in this worker build',\n\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t})\n}\n\nfunction handleMessage(request: WorkerRequest): void {\n\ttry {\n\t\tswitch (request.type) {\n\t\t\tcase 'open':\n\t\t\t\t// open is async due to WASM loading\n\t\t\t\thandleOpen(request.id, request.ddlStatements, request.dbName)\n\t\t\t\treturn\n\t\t\tcase 'close':\n\t\t\t\thandleClose(request.id)\n\t\t\t\treturn\n\t\t\tcase 'execute':\n\t\t\t\thandleExecute(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'query':\n\t\t\t\thandleQuery(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'begin':\n\t\t\t\thandleExecute(request.id, 'BEGIN')\n\t\t\t\treturn\n\t\t\tcase 'commit':\n\t\t\t\thandleExecute(request.id, 'COMMIT')\n\t\t\t\treturn\n\t\t\tcase 'rollback':\n\t\t\t\thandleExecute(request.id, 'ROLLBACK')\n\t\t\t\treturn\n\t\t\tcase 'migrate':\n\t\t\t\tif (!db) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: 'Database is not open',\n\t\t\t\t\t\tcode: 'DB_NOT_OPEN',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tfor (const sql of request.statements) {\n\t\t\t\t\t\tdb.exec({ sql })\n\t\t\t\t\t}\n\t\t\t\t\tsendResponse({ id: request.id, type: 'success' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: (error as Error).message,\n\t\t\t\t\t\tcode: 'MIGRATE_ERROR',\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase 'export':\n\t\t\t\t// Export is not trivially supported via the oo1 API in the browser.\n\t\t\t\t// In a real implementation, we'd use the C API's sqlite3_serialize.\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Export not yet supported in browser worker',\n\t\t\t\t\tcode: 'EXPORT_NOT_SUPPORTED',\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase 'import':\n\t\t\t\thandleImport(request.id, request.data)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: (request as WorkerRequest).id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Unknown request type',\n\t\t\t\t\tcode: 'UNKNOWN_REQUEST',\n\t\t\t\t})\n\t\t}\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid: request.id,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'WORKER_ERROR',\n\t\t})\n\t}\n}\n\n// Listen for messages from the main thread\nself.onmessage = (event: MessageEvent<WorkerRequest>) => {\n\thandleMessage(event.data)\n}\n"],"mappings":"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","names":[]}
@@ -75,6 +75,7 @@ async function handleOpen(id, ddlStatements, dbName) {
75
75
  );
76
76
  sqlite3Api = sqlite3;
77
77
  let useOpfs = false;
78
+ let fallbackReason;
78
79
  if (sqlite3.installOpfsSAHPoolVfs) {
79
80
  try {
80
81
  const pool = await withTimeout(
@@ -84,9 +85,14 @@ async function handleOpen(id, ddlStatements, dbName) {
84
85
  );
85
86
  db = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? "kora-db"));
86
87
  useOpfs = true;
87
- } catch {
88
- console.warn("[kora] OPFS unavailable, falling back to in-memory SQLite");
88
+ } catch (error) {
89
+ fallbackReason = classifyOpfsFailure(error);
90
+ console.warn(
91
+ `[kora] OPFS unavailable (${fallbackReason}), falling back to in-memory SQLite`
92
+ );
89
93
  }
94
+ } else {
95
+ fallbackReason = "unsupported";
90
96
  }
91
97
  if (!useOpfs) {
92
98
  db = new sqlite3.oo1.DB({ filename: ":memory:" });
@@ -107,7 +113,11 @@ async function handleOpen(id, ddlStatements, dbName) {
107
113
  db?.exec({ sql });
108
114
  }
109
115
  }
110
- sendResponse({ id, type: "success" });
116
+ sendResponse({
117
+ id,
118
+ type: "success",
119
+ data: { persistent: useOpfs, ...fallbackReason ? { fallbackReason } : {} }
120
+ });
111
121
  } catch (error) {
112
122
  sendResponse({
113
123
  id,
@@ -117,6 +127,18 @@ async function handleOpen(id, ddlStatements, dbName) {
117
127
  });
118
128
  }
119
129
  }
130
+ function classifyOpfsFailure(error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ if (/timed out|timeout/i.test(message)) {
133
+ return "timeout";
134
+ }
135
+ if (/lock|already|in use|in-use|NoModificationAllowed|acquire|SyncAccessHandle|being created/i.test(
136
+ message
137
+ )) {
138
+ return "lock-conflict";
139
+ }
140
+ return "unsupported";
141
+ }
120
142
  function handleClose(id) {
121
143
  if (db) {
122
144
  db.close();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/sqlite-wasm-worker.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * Web Worker script for running SQLite WASM.\n *\n * This file is intended to run inside a Web Worker in browsers.\n * It loads @sqlite.org/sqlite-wasm, initializes SQLite with OPFS persistence\n * (falling back to in-memory if unavailable), and processes messages from\n * the main thread via the WorkerRequest/WorkerResponse protocol.\n *\n * This script cannot be tested in Node.js — it is validated in E2E browser tests.\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\ninterface SqliteDb {\n\texec(opts: {\n\t\tsql: string\n\t\tbind?: unknown[]\n\t\treturnValue?: string\n\t\trowMode?: string\n\t\tcallback?: (row: Record<string, unknown>) => void\n\t}): void\n\tclose(): void\n\tdeserialize?: (data: Uint8Array) => void\n}\n\ndeclare const self: DedicatedWorkerGlobalScope\n\nlet db: SqliteDb | null = null\nlet sqlite3Api: unknown = null\n\nfunction sendResponse(response: WorkerResponse): void {\n\tself.postMessage(response)\n}\n\nfunction handleExecute(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tdb.exec({ sql, bind: params })\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'EXEC_ERROR' })\n\t}\n}\n\nfunction handleQuery(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tconst rows: Record<string, unknown>[] = []\n\t\tdb.exec({\n\t\t\tsql,\n\t\t\tbind: params,\n\t\t\trowMode: 'object',\n\t\t\tcallback: (row: Record<string, unknown>) => {\n\t\t\t\trows.push({ ...row })\n\t\t\t},\n\t\t})\n\t\tsendResponse({ id, type: 'success', data: rows })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'QUERY_ERROR' })\n\t}\n}\n\nfunction opfsDatabaseFilename(dbName: string): string {\n\tconst base = dbName.replace(/[^a-zA-Z0-9._-]/g, '_')\n\treturn base.endsWith('.db') ? base : `${base}.db`\n}\n\n/** Headless browsers and some profiles hang on OPFS VFS install; fall back to memory. */\nconst OPFS_INIT_TIMEOUT_MS = 10_000\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tpromise,\n\t\t\tnew Promise<T>((_, reject) => {\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t)\n\t\t\t}),\n\t\t])\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer)\n\t}\n}\n\nasync function handleOpen(id: number, ddlStatements: string[], dbName?: string): Promise<void> {\n\ttry {\n\t\tconst sqlite3InitModule = (await import('@sqlite.org/sqlite-wasm')).default\n\n\t\t// In production builds, Vite hashes asset filenames (e.g. sqlite3-[hash].wasm).\n\t\t// The sqlite3 module's default locateFile resolves the unhashed name, causing a 404.\n\t\t// Templates set __KORA_SQLITE_WASM_URL via `import wasmUrl from '...sqlite3.wasm?url'`\n\t\t// so we can override locateFile with the correct hashed URL.\n\t\tconst wasmUrl = (globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL as\n\t\t\t| string\n\t\t\t| undefined\n\t\tconst initOptions = wasmUrl\n\t\t\t? {\n\t\t\t\t\tlocateFile: (file: string) => {\n\t\t\t\t\t\tif (file.endsWith('.wasm')) return wasmUrl\n\t\t\t\t\t\treturn file\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: undefined\n\n\t\t// sqlite3InitModule accepts Emscripten Module overrides but the types don't reflect this\n\t\tconst initFn = sqlite3InitModule as unknown as (\n\t\t\topts?: Record<string, unknown>,\n\t\t) => Promise<unknown>\n\t\tconst sqlite3 = (await withTimeout(\n\t\t\tinitFn(initOptions),\n\t\t\t60_000,\n\t\t\t'SQLite3 module init',\n\t\t)) as Awaited<ReturnType<typeof sqlite3InitModule>>\n\t\tsqlite3Api = sqlite3\n\n\t\t// Try OPFS persistence first\n\t\tlet useOpfs = false\n\t\tif (sqlite3.installOpfsSAHPoolVfs) {\n\t\t\ttry {\n\t\t\t\tconst pool = await withTimeout(\n\t\t\t\t\tsqlite3.installOpfsSAHPoolVfs({ name: 'kora-opfs' }),\n\t\t\t\t\tOPFS_INIT_TIMEOUT_MS,\n\t\t\t\t\t'OPFS VFS install',\n\t\t\t\t)\n\t\t\t\tdb = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? 'kora-db'))\n\t\t\t\tuseOpfs = true\n\t\t\t} catch {\n\t\t\t\t// OPFS unavailable or too slow (common in headless CI), fall back to in-memory\n\t\t\t\tconsole.warn('[kora] OPFS unavailable, falling back to in-memory SQLite')\n\t\t\t}\n\t\t}\n\n\t\tif (!useOpfs) {\n\t\t\tdb = new sqlite3.oo1.DB({ filename: ':memory:' })\n\t\t}\n\n\t\t// Set pragmas\n\t\tdb?.exec({ sql: 'PRAGMA journal_mode = WAL' })\n\t\tdb?.exec({ sql: 'PRAGMA foreign_keys = ON' })\n\n\t\t// Execute DDL statements\n\t\tfor (const sql of ddlStatements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tdb?.exec({ sql: sql.replace('--kora:safe-alter\\n', '') })\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\tif (!msg.includes('duplicate column name')) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdb?.exec({ sql })\n\t\t\t}\n\t\t}\n\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'INIT_ERROR',\n\t\t})\n\t}\n}\n\nfunction handleClose(id: number): void {\n\tif (db) {\n\t\tdb.close()\n\t\tdb = null\n\t}\n\tsendResponse({ id, type: 'success' })\n}\n\nfunction handleImport(id: number, data: Uint8Array): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\n\tconst dbWithDeserialize = db as SqliteDb & { deserialize?: (bytes: Uint8Array) => void }\n\tif (typeof dbWithDeserialize.deserialize === 'function') {\n\t\ttry {\n\t\t\tdbWithDeserialize.deserialize(data)\n\t\t\tsendResponse({ id, type: 'success' })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'IMPORT_ERROR' })\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst sqlite3 = sqlite3Api as {\n\t\too1?: { DB?: new (...args: unknown[]) => SqliteDb }\n\t\tcapi?: { sqlite3_deserialize?: unknown }\n\t} | null\n\n\tif (!sqlite3 || typeof sqlite3.capi?.sqlite3_deserialize === 'undefined') {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: 'Import not supported in this SQLite WASM runtime',\n\t\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t\t})\n\t\treturn\n\t}\n\n\tsendResponse({\n\t\tid,\n\t\ttype: 'error',\n\t\tmessage:\n\t\t\t'Import requires runtime-specific deserialize wiring and is unavailable in this worker build',\n\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t})\n}\n\nfunction handleMessage(request: WorkerRequest): void {\n\ttry {\n\t\tswitch (request.type) {\n\t\t\tcase 'open':\n\t\t\t\t// open is async due to WASM loading\n\t\t\t\thandleOpen(request.id, request.ddlStatements, request.dbName)\n\t\t\t\treturn\n\t\t\tcase 'close':\n\t\t\t\thandleClose(request.id)\n\t\t\t\treturn\n\t\t\tcase 'execute':\n\t\t\t\thandleExecute(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'query':\n\t\t\t\thandleQuery(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'begin':\n\t\t\t\thandleExecute(request.id, 'BEGIN')\n\t\t\t\treturn\n\t\t\tcase 'commit':\n\t\t\t\thandleExecute(request.id, 'COMMIT')\n\t\t\t\treturn\n\t\t\tcase 'rollback':\n\t\t\t\thandleExecute(request.id, 'ROLLBACK')\n\t\t\t\treturn\n\t\t\tcase 'migrate':\n\t\t\t\tif (!db) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: 'Database is not open',\n\t\t\t\t\t\tcode: 'DB_NOT_OPEN',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tfor (const sql of request.statements) {\n\t\t\t\t\t\tdb.exec({ sql })\n\t\t\t\t\t}\n\t\t\t\t\tsendResponse({ id: request.id, type: 'success' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: (error as Error).message,\n\t\t\t\t\t\tcode: 'MIGRATE_ERROR',\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase 'export':\n\t\t\t\t// Export is not trivially supported via the oo1 API in the browser.\n\t\t\t\t// In a real implementation, we'd use the C API's sqlite3_serialize.\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Export not yet supported in browser worker',\n\t\t\t\t\tcode: 'EXPORT_NOT_SUPPORTED',\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase 'import':\n\t\t\t\thandleImport(request.id, request.data)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: (request as WorkerRequest).id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Unknown request type',\n\t\t\t\t\tcode: 'UNKNOWN_REQUEST',\n\t\t\t\t})\n\t\t}\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid: request.id,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'WORKER_ERROR',\n\t\t})\n\t}\n}\n\n// Listen for messages from the main thread\nself.onmessage = (event: MessageEvent<WorkerRequest>) => {\n\thandleMessage(event.data)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/sqlite-wasm-worker.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * Web Worker script for running SQLite WASM.\n *\n * This file is intended to run inside a Web Worker in browsers.\n * It loads @sqlite.org/sqlite-wasm, initializes SQLite with OPFS persistence\n * (falling back to in-memory if unavailable), and processes messages from\n * the main thread via the WorkerRequest/WorkerResponse protocol.\n *\n * This script cannot be tested in Node.js — it is validated in E2E browser tests.\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\ninterface SqliteDb {\n\texec(opts: {\n\t\tsql: string\n\t\tbind?: unknown[]\n\t\treturnValue?: string\n\t\trowMode?: string\n\t\tcallback?: (row: Record<string, unknown>) => void\n\t}): void\n\tclose(): void\n\tdeserialize?: (data: Uint8Array) => void\n}\n\ndeclare const self: DedicatedWorkerGlobalScope\n\nlet db: SqliteDb | null = null\nlet sqlite3Api: unknown = null\n\nfunction sendResponse(response: WorkerResponse): void {\n\tself.postMessage(response)\n}\n\nfunction handleExecute(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tdb.exec({ sql, bind: params })\n\t\tsendResponse({ id, type: 'success' })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'EXEC_ERROR' })\n\t}\n}\n\nfunction handleQuery(id: number, sql: string, params?: unknown[]): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\ttry {\n\t\tconst rows: Record<string, unknown>[] = []\n\t\tdb.exec({\n\t\t\tsql,\n\t\t\tbind: params,\n\t\t\trowMode: 'object',\n\t\t\tcallback: (row: Record<string, unknown>) => {\n\t\t\t\trows.push({ ...row })\n\t\t\t},\n\t\t})\n\t\tsendResponse({ id, type: 'success', data: rows })\n\t} catch (error) {\n\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'QUERY_ERROR' })\n\t}\n}\n\nfunction opfsDatabaseFilename(dbName: string): string {\n\tconst base = dbName.replace(/[^a-zA-Z0-9._-]/g, '_')\n\treturn base.endsWith('.db') ? base : `${base}.db`\n}\n\n/** Headless browsers and some profiles hang on OPFS VFS install; fall back to memory. */\nconst OPFS_INIT_TIMEOUT_MS = 10_000\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tpromise,\n\t\t\tnew Promise<T>((_, reject) => {\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t)\n\t\t\t}),\n\t\t])\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer)\n\t}\n}\n\nasync function handleOpen(id: number, ddlStatements: string[], dbName?: string): Promise<void> {\n\ttry {\n\t\tconst sqlite3InitModule = (await import('@sqlite.org/sqlite-wasm')).default\n\n\t\t// In production builds, Vite hashes asset filenames (e.g. sqlite3-[hash].wasm).\n\t\t// The sqlite3 module's default locateFile resolves the unhashed name, causing a 404.\n\t\t// Templates set __KORA_SQLITE_WASM_URL via `import wasmUrl from '...sqlite3.wasm?url'`\n\t\t// so we can override locateFile with the correct hashed URL.\n\t\tconst wasmUrl = (globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL as\n\t\t\t| string\n\t\t\t| undefined\n\t\tconst initOptions = wasmUrl\n\t\t\t? {\n\t\t\t\t\tlocateFile: (file: string) => {\n\t\t\t\t\t\tif (file.endsWith('.wasm')) return wasmUrl\n\t\t\t\t\t\treturn file\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: undefined\n\n\t\t// sqlite3InitModule accepts Emscripten Module overrides but the types don't reflect this\n\t\tconst initFn = sqlite3InitModule as unknown as (\n\t\t\topts?: Record<string, unknown>,\n\t\t) => Promise<unknown>\n\t\tconst sqlite3 = (await withTimeout(\n\t\t\tinitFn(initOptions),\n\t\t\t60_000,\n\t\t\t'SQLite3 module init',\n\t\t)) as Awaited<ReturnType<typeof sqlite3InitModule>>\n\t\tsqlite3Api = sqlite3\n\n\t\t// Try OPFS persistence first\n\t\tlet useOpfs = false\n\t\tlet fallbackReason: 'lock-conflict' | 'timeout' | 'unsupported' | undefined\n\t\tif (sqlite3.installOpfsSAHPoolVfs) {\n\t\t\ttry {\n\t\t\t\tconst pool = await withTimeout(\n\t\t\t\t\tsqlite3.installOpfsSAHPoolVfs({ name: 'kora-opfs' }),\n\t\t\t\t\tOPFS_INIT_TIMEOUT_MS,\n\t\t\t\t\t'OPFS VFS install',\n\t\t\t\t)\n\t\t\t\tdb = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? 'kora-db'))\n\t\t\t\tuseOpfs = true\n\t\t\t} catch (error) {\n\t\t\t\t// OPFS unavailable. Classify WHY so the main thread can surface a clear\n\t\t\t\t// diagnostic instead of a silent, non-persistent in-memory fallback that\n\t\t\t\t// loses the user's data on reload.\n\t\t\t\tfallbackReason = classifyOpfsFailure(error)\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[kora] OPFS unavailable (${fallbackReason}), falling back to in-memory SQLite`,\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tfallbackReason = 'unsupported'\n\t\t}\n\n\t\tif (!useOpfs) {\n\t\t\tdb = new sqlite3.oo1.DB({ filename: ':memory:' })\n\t\t}\n\n\t\t// Set pragmas\n\t\tdb?.exec({ sql: 'PRAGMA journal_mode = WAL' })\n\t\tdb?.exec({ sql: 'PRAGMA foreign_keys = ON' })\n\n\t\t// Execute DDL statements\n\t\tfor (const sql of ddlStatements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tdb?.exec({ sql: sql.replace('--kora:safe-alter\\n', '') })\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\tif (!msg.includes('duplicate column name')) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdb?.exec({ sql })\n\t\t\t}\n\t\t}\n\n\t\t// Report the storage mode so the adapter can emit `store:opfs-unavailable`\n\t\t// when persistence silently degraded to in-memory.\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'success',\n\t\t\tdata: { persistent: useOpfs, ...(fallbackReason ? { fallbackReason } : {}) },\n\t\t})\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'INIT_ERROR',\n\t\t})\n\t}\n}\n\n/**\n * Classify why the OPFS SyncAccessHandle pool could not be installed, so the\n * fallback to in-memory is explainable rather than silent.\n *\n * A `lock-conflict` is the important case: another runtime on the same origin\n * (another tab or a logically separate app that reused the database name) already\n * holds the pool's access handles, so this runtime cannot persist.\n */\nfunction classifyOpfsFailure(error: unknown): 'lock-conflict' | 'timeout' | 'unsupported' {\n\tconst message = error instanceof Error ? error.message : String(error)\n\tif (/timed out|timeout/i.test(message)) {\n\t\treturn 'timeout'\n\t}\n\tif (\n\t\t/lock|already|in use|in-use|NoModificationAllowed|acquire|SyncAccessHandle|being created/i.test(\n\t\t\tmessage,\n\t\t)\n\t) {\n\t\treturn 'lock-conflict'\n\t}\n\treturn 'unsupported'\n}\n\nfunction handleClose(id: number): void {\n\tif (db) {\n\t\tdb.close()\n\t\tdb = null\n\t}\n\tsendResponse({ id, type: 'success' })\n}\n\nfunction handleImport(id: number, data: Uint8Array): void {\n\tif (!db) {\n\t\tsendResponse({ id, type: 'error', message: 'Database is not open', code: 'DB_NOT_OPEN' })\n\t\treturn\n\t}\n\n\tconst dbWithDeserialize = db as SqliteDb & { deserialize?: (bytes: Uint8Array) => void }\n\tif (typeof dbWithDeserialize.deserialize === 'function') {\n\t\ttry {\n\t\t\tdbWithDeserialize.deserialize(data)\n\t\t\tsendResponse({ id, type: 'success' })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tsendResponse({ id, type: 'error', message: (error as Error).message, code: 'IMPORT_ERROR' })\n\t\t\treturn\n\t\t}\n\t}\n\n\tconst sqlite3 = sqlite3Api as {\n\t\too1?: { DB?: new (...args: unknown[]) => SqliteDb }\n\t\tcapi?: { sqlite3_deserialize?: unknown }\n\t} | null\n\n\tif (!sqlite3 || typeof sqlite3.capi?.sqlite3_deserialize === 'undefined') {\n\t\tsendResponse({\n\t\t\tid,\n\t\t\ttype: 'error',\n\t\t\tmessage: 'Import not supported in this SQLite WASM runtime',\n\t\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t\t})\n\t\treturn\n\t}\n\n\tsendResponse({\n\t\tid,\n\t\ttype: 'error',\n\t\tmessage:\n\t\t\t'Import requires runtime-specific deserialize wiring and is unavailable in this worker build',\n\t\tcode: 'IMPORT_NOT_SUPPORTED',\n\t})\n}\n\nfunction handleMessage(request: WorkerRequest): void {\n\ttry {\n\t\tswitch (request.type) {\n\t\t\tcase 'open':\n\t\t\t\t// open is async due to WASM loading\n\t\t\t\thandleOpen(request.id, request.ddlStatements, request.dbName)\n\t\t\t\treturn\n\t\t\tcase 'close':\n\t\t\t\thandleClose(request.id)\n\t\t\t\treturn\n\t\t\tcase 'execute':\n\t\t\t\thandleExecute(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'query':\n\t\t\t\thandleQuery(request.id, request.sql, request.params)\n\t\t\t\treturn\n\t\t\tcase 'begin':\n\t\t\t\thandleExecute(request.id, 'BEGIN')\n\t\t\t\treturn\n\t\t\tcase 'commit':\n\t\t\t\thandleExecute(request.id, 'COMMIT')\n\t\t\t\treturn\n\t\t\tcase 'rollback':\n\t\t\t\thandleExecute(request.id, 'ROLLBACK')\n\t\t\t\treturn\n\t\t\tcase 'migrate':\n\t\t\t\tif (!db) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: 'Database is not open',\n\t\t\t\t\t\tcode: 'DB_NOT_OPEN',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tfor (const sql of request.statements) {\n\t\t\t\t\t\tdb.exec({ sql })\n\t\t\t\t\t}\n\t\t\t\t\tsendResponse({ id: request.id, type: 'success' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendResponse({\n\t\t\t\t\t\tid: request.id,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t\tmessage: (error as Error).message,\n\t\t\t\t\t\tcode: 'MIGRATE_ERROR',\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase 'export':\n\t\t\t\t// Export is not trivially supported via the oo1 API in the browser.\n\t\t\t\t// In a real implementation, we'd use the C API's sqlite3_serialize.\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Export not yet supported in browser worker',\n\t\t\t\t\tcode: 'EXPORT_NOT_SUPPORTED',\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase 'import':\n\t\t\t\thandleImport(request.id, request.data)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tsendResponse({\n\t\t\t\t\tid: (request as WorkerRequest).id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Unknown request type',\n\t\t\t\t\tcode: 'UNKNOWN_REQUEST',\n\t\t\t\t})\n\t\t}\n\t} catch (error) {\n\t\tsendResponse({\n\t\t\tid: request.id,\n\t\t\ttype: 'error',\n\t\t\tmessage: (error as Error).message,\n\t\t\tcode: 'WORKER_ERROR',\n\t\t})\n\t}\n}\n\n// Listen for messages from the main thread\nself.onmessage = (event: MessageEvent<WorkerRequest>) => {\n\thandleMessage(event.data)\n}\n"],"mappings":";AA4BA,IAAI,KAAsB;AAC1B,IAAI,aAAsB;AAE1B,SAAS,aAAa,UAAgC;AACrD,OAAK,YAAY,QAAQ;AAC1B;AAEA,SAAS,cAAc,IAAY,KAAa,QAA0B;AACzE,MAAI,CAAC,IAAI;AACR,iBAAa,EAAE,IAAI,MAAM,SAAS,SAAS,wBAAwB,MAAM,cAAc,CAAC;AACxF;AAAA,EACD;AACA,MAAI;AACH,OAAG,KAAK,EAAE,KAAK,MAAM,OAAO,CAAC;AAC7B,iBAAa,EAAE,IAAI,MAAM,UAAU,CAAC;AAAA,EACrC,SAAS,OAAO;AACf,iBAAa,EAAE,IAAI,MAAM,SAAS,SAAU,MAAgB,SAAS,MAAM,aAAa,CAAC;AAAA,EAC1F;AACD;AAEA,SAAS,YAAY,IAAY,KAAa,QAA0B;AACvE,MAAI,CAAC,IAAI;AACR,iBAAa,EAAE,IAAI,MAAM,SAAS,SAAS,wBAAwB,MAAM,cAAc,CAAC;AACxF;AAAA,EACD;AACA,MAAI;AACH,UAAM,OAAkC,CAAC;AACzC,OAAG,KAAK;AAAA,MACP;AAAA,MACA,MAAM;AAAA,MACN,SAAS;AAAA,MACT,UAAU,CAAC,QAAiC;AAC3C,aAAK,KAAK,EAAE,GAAG,IAAI,CAAC;AAAA,MACrB;AAAA,IACD,CAAC;AACD,iBAAa,EAAE,IAAI,MAAM,WAAW,MAAM,KAAK,CAAC;AAAA,EACjD,SAAS,OAAO;AACf,iBAAa,EAAE,IAAI,MAAM,SAAS,SAAU,MAAgB,SAAS,MAAM,cAAc,CAAC;AAAA,EAC3F;AACD;AAEA,SAAS,qBAAqB,QAAwB;AACrD,QAAM,OAAO,OAAO,QAAQ,oBAAoB,GAAG;AACnD,SAAO,KAAK,SAAS,KAAK,IAAI,OAAO,GAAG,IAAI;AAC7C;AAGA,IAAM,uBAAuB;AAE7B,eAAe,YAAe,SAAqB,WAAmB,OAA2B;AAChG,MAAI;AACJ,MAAI;AACH,WAAO,MAAM,QAAQ,KAAK;AAAA,MACzB;AAAA,MACA,IAAI,QAAW,CAAC,GAAG,WAAW;AAC7B,gBAAQ;AAAA,UACP,MAAM,OAAO,IAAI,MAAM,GAAG,KAAK,oBAAoB,SAAS,IAAI,CAAC;AAAA,UACjE;AAAA,QACD;AAAA,MACD,CAAC;AAAA,IACF,CAAC;AAAA,EACF,UAAE;AACD,QAAI,UAAU,OAAW,cAAa,KAAK;AAAA,EAC5C;AACD;AAEA,eAAe,WAAW,IAAY,eAAyB,QAAgC;AAC9F,MAAI;AACH,UAAM,qBAAqB,MAAM,OAAO,yBAAyB,GAAG;AAMpE,UAAM,UAAW,WAAuC;AAGxD,UAAM,cAAc,UACjB;AAAA,MACA,YAAY,CAAC,SAAiB;AAC7B,YAAI,KAAK,SAAS,OAAO,EAAG,QAAO;AACnC,eAAO;AAAA,MACR;AAAA,IACD,IACC;AAGH,UAAM,SAAS;AAGf,UAAM,UAAW,MAAM;AAAA,MACtB,OAAO,WAAW;AAAA,MAClB;AAAA,MACA;AAAA,IACD;AACA,iBAAa;AAGb,QAAI,UAAU;AACd,QAAI;AACJ,QAAI,QAAQ,uBAAuB;AAClC,UAAI;AACH,cAAM,OAAO,MAAM;AAAA,UAClB,QAAQ,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAAA,UACnD;AAAA,UACA;AAAA,QACD;AACA,aAAK,IAAI,KAAK,cAAc,qBAAqB,UAAU,SAAS,CAAC;AACrE,kBAAU;AAAA,MACX,SAAS,OAAO;AAIf,yBAAiB,oBAAoB,KAAK;AAC1C,gBAAQ;AAAA,UACP,4BAA4B,cAAc;AAAA,QAC3C;AAAA,MACD;AAAA,IACD,OAAO;AACN,uBAAiB;AAAA,IAClB;AAEA,QAAI,CAAC,SAAS;AACb,WAAK,IAAI,QAAQ,IAAI,GAAG,EAAE,UAAU,WAAW,CAAC;AAAA,IACjD;AAGA,QAAI,KAAK,EAAE,KAAK,4BAA4B,CAAC;AAC7C,QAAI,KAAK,EAAE,KAAK,2BAA2B,CAAC;AAG5C,eAAW,OAAO,eAAe;AAChC,UAAI,IAAI,WAAW,mBAAmB,GAAG;AAExC,YAAI;AACH,cAAI,KAAK,EAAE,KAAK,IAAI,QAAQ,uBAAuB,EAAE,EAAE,CAAC;AAAA,QACzD,SAAS,GAAG;AACX,gBAAM,MAAO,EAAY,WAAW;AACpC,cAAI,CAAC,IAAI,SAAS,uBAAuB,GAAG;AAC3C,kBAAM;AAAA,UACP;AAAA,QACD;AAAA,MACD,OAAO;AACN,YAAI,KAAK,EAAE,IAAI,CAAC;AAAA,MACjB;AAAA,IACD;AAIA,iBAAa;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,MACN,MAAM,EAAE,YAAY,SAAS,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC,EAAG;AAAA,IAC5E,CAAC;AAAA,EACF,SAAS,OAAO;AACf,iBAAa;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,MACN,SAAU,MAAgB;AAAA,MAC1B,MAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AAUA,SAAS,oBAAoB,OAA6D;AACzF,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,MAAI,qBAAqB,KAAK,OAAO,GAAG;AACvC,WAAO;AAAA,EACR;AACA,MACC,2FAA2F;AAAA,IAC1F;AAAA,EACD,GACC;AACD,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAEA,SAAS,YAAY,IAAkB;AACtC,MAAI,IAAI;AACP,OAAG,MAAM;AACT,SAAK;AAAA,EACN;AACA,eAAa,EAAE,IAAI,MAAM,UAAU,CAAC;AACrC;AAEA,SAAS,aAAa,IAAY,MAAwB;AACzD,MAAI,CAAC,IAAI;AACR,iBAAa,EAAE,IAAI,MAAM,SAAS,SAAS,wBAAwB,MAAM,cAAc,CAAC;AACxF;AAAA,EACD;AAEA,QAAM,oBAAoB;AAC1B,MAAI,OAAO,kBAAkB,gBAAgB,YAAY;AACxD,QAAI;AACH,wBAAkB,YAAY,IAAI;AAClC,mBAAa,EAAE,IAAI,MAAM,UAAU,CAAC;AACpC;AAAA,IACD,SAAS,OAAO;AACf,mBAAa,EAAE,IAAI,MAAM,SAAS,SAAU,MAAgB,SAAS,MAAM,eAAe,CAAC;AAC3F;AAAA,IACD;AAAA,EACD;AAEA,QAAM,UAAU;AAKhB,MAAI,CAAC,WAAW,OAAO,QAAQ,MAAM,wBAAwB,aAAa;AACzE,iBAAa;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,IACP,CAAC;AACD;AAAA,EACD;AAEA,eAAa;AAAA,IACZ;AAAA,IACA,MAAM;AAAA,IACN,SACC;AAAA,IACD,MAAM;AAAA,EACP,CAAC;AACF;AAEA,SAAS,cAAc,SAA8B;AACpD,MAAI;AACH,YAAQ,QAAQ,MAAM;AAAA,MACrB,KAAK;AAEJ,mBAAW,QAAQ,IAAI,QAAQ,eAAe,QAAQ,MAAM;AAC5D;AAAA,MACD,KAAK;AACJ,oBAAY,QAAQ,EAAE;AACtB;AAAA,MACD,KAAK;AACJ,sBAAc,QAAQ,IAAI,QAAQ,KAAK,QAAQ,MAAM;AACrD;AAAA,MACD,KAAK;AACJ,oBAAY,QAAQ,IAAI,QAAQ,KAAK,QAAQ,MAAM;AACnD;AAAA,MACD,KAAK;AACJ,sBAAc,QAAQ,IAAI,OAAO;AACjC;AAAA,MACD,KAAK;AACJ,sBAAc,QAAQ,IAAI,QAAQ;AAClC;AAAA,MACD,KAAK;AACJ,sBAAc,QAAQ,IAAI,UAAU;AACpC;AAAA,MACD,KAAK;AACJ,YAAI,CAAC,IAAI;AACR,uBAAa;AAAA,YACZ,IAAI,QAAQ;AAAA,YACZ,MAAM;AAAA,YACN,SAAS;AAAA,YACT,MAAM;AAAA,UACP,CAAC;AACD;AAAA,QACD;AACA,YAAI;AACH,qBAAW,OAAO,QAAQ,YAAY;AACrC,eAAG,KAAK,EAAE,IAAI,CAAC;AAAA,UAChB;AACA,uBAAa,EAAE,IAAI,QAAQ,IAAI,MAAM,UAAU,CAAC;AAAA,QACjD,SAAS,OAAO;AACf,uBAAa;AAAA,YACZ,IAAI,QAAQ;AAAA,YACZ,MAAM;AAAA,YACN,SAAU,MAAgB;AAAA,YAC1B,MAAM;AAAA,UACP,CAAC;AAAA,QACF;AACA;AAAA,MACD,KAAK;AAGJ,qBAAa;AAAA,UACZ,IAAI,QAAQ;AAAA,UACZ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,MAAM;AAAA,QACP,CAAC;AACD;AAAA,MACD,KAAK;AACJ,qBAAa,QAAQ,IAAI,QAAQ,IAAI;AACrC;AAAA,MACD;AACC,qBAAa;AAAA,UACZ,IAAK,QAA0B;AAAA,UAC/B,MAAM;AAAA,UACN,SAAS;AAAA,UACT,MAAM;AAAA,QACP,CAAC;AAAA,IACH;AAAA,EACD,SAAS,OAAO;AACf,iBAAa;AAAA,MACZ,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,SAAU,MAAgB;AAAA,MAC1B,MAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AAGA,KAAK,YAAY,CAAC,UAAuC;AACxD,gBAAc,MAAM,IAAI;AACzB;","names":[]}
@@ -413,6 +413,7 @@ var SqliteWasmAdapter = class {
413
413
  sharedWorkerUrl;
414
414
  workerResponseTimeoutMs;
415
415
  dbName;
416
+ emitter;
416
417
  tabSession = null;
417
418
  sharedWorker = null;
418
419
  constructor(options = {}) {
@@ -421,6 +422,7 @@ var SqliteWasmAdapter = class {
421
422
  this.sharedWorkerUrl = options.sharedWorkerUrl;
422
423
  this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
423
424
  this.dbName = options.dbName ?? "kora-db";
425
+ this.emitter = options.emitter;
424
426
  }
425
427
  async open(schema) {
426
428
  if (this.opened) return;
@@ -443,6 +445,11 @@ var SqliteWasmAdapter = class {
443
445
  this.tabSession.channelName,
444
446
  this.workerResponseTimeoutMs
445
447
  );
448
+ this.emitter?.emit({
449
+ type: "store:db-name-collision",
450
+ dbName: this.dbName,
451
+ message: `Another runtime on this origin is already using database "${this.dbName}"; this runtime is sharing it as a follower. If these are separate apps, give each a distinct store name.`
452
+ });
446
453
  }
447
454
  } else {
448
455
  throw new AdapterError(
@@ -462,8 +469,30 @@ var SqliteWasmAdapter = class {
462
469
  dbName: this.dbName
463
470
  });
464
471
  }
472
+ this.reportStorageMode(response.data);
465
473
  this.opened = true;
466
474
  }
475
+ /**
476
+ * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
477
+ * persistence silently degraded to an in-memory database, so a data-loss
478
+ * condition is observable instead of failing silently. A persistent open (or a
479
+ * bridge that does not report a mode, e.g. the Node mock) emits nothing.
480
+ */
481
+ reportStorageMode(data) {
482
+ if (!this.emitter || typeof data !== "object" || data === null) {
483
+ return;
484
+ }
485
+ const mode = data;
486
+ if (mode.persistent === false) {
487
+ const reason = mode.fallbackReason ?? "unsupported";
488
+ this.emitter.emit({
489
+ type: "store:opfs-unavailable",
490
+ dbName: this.dbName,
491
+ reason,
492
+ message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
493
+ });
494
+ }
495
+ }
467
496
  async close() {
468
497
  if (!this.bridge) return;
469
498
  try {