@korajs/store 1.0.0-beta.1 → 1.0.0-beta.4

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 (58) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.js +1 -1
  3. package/dist/adapters/indexeddb.cjs +196 -39
  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 +15 -8
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs +80 -53
  10. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -1
  11. package/dist/adapters/sqlite-wasm-shared-host.d.cts +63 -1
  12. package/dist/adapters/sqlite-wasm-shared-host.d.ts +63 -1
  13. package/dist/adapters/sqlite-wasm-shared-host.js +56 -52
  14. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -1
  15. package/dist/adapters/sqlite-wasm-worker.cjs +25 -3
  16. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm-worker.js +25 -3
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  19. package/dist/adapters/sqlite-wasm.cjs +186 -34
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm.d.cts +29 -3
  22. package/dist/adapters/sqlite-wasm.d.ts +29 -3
  23. package/dist/adapters/sqlite-wasm.js +3 -3
  24. package/dist/{backup-ZV4Y424B.js → backup-IQE4QC7Q.js} +2 -2
  25. package/dist/{chunk-RBPVEAVM.js → chunk-2ZRQGFEF.js} +179 -36
  26. package/dist/chunk-2ZRQGFEF.js.map +1 -0
  27. package/dist/{chunk-IOEFNHHP.js → chunk-COJSQBXW.js} +2 -2
  28. package/dist/{chunk-X57MWW35.js → chunk-OCOGQRHW.js} +12 -9
  29. package/dist/chunk-OCOGQRHW.js.map +1 -0
  30. package/dist/{chunk-FFYA7J5H.js → chunk-RCTG3IJG.js} +3 -2
  31. package/dist/chunk-RCTG3IJG.js.map +1 -0
  32. package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
  33. package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
  34. package/dist/{chunk-ZLWAYQEQ.js → chunk-UXRWVXBP.js} +32 -28
  35. package/dist/chunk-UXRWVXBP.js.map +1 -0
  36. package/dist/{export-audit-AJ7RORNK.js → export-audit-MT2LTMIN.js} +2 -2
  37. package/dist/index.cjs +167 -141
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +10 -1
  40. package/dist/index.d.ts +10 -1
  41. package/dist/index.js +51 -41
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal.cjs +45 -40
  44. package/dist/internal.cjs.map +1 -1
  45. package/dist/internal.js +2 -2
  46. package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-CNHRf-m2.d.cts} +1 -1
  47. package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-CNHRf-m2.d.ts} +1 -1
  48. package/dist/sqlite-wasm-channel-NGSBJR44.js +12 -0
  49. package/package.json +2 -2
  50. package/dist/chunk-FFYA7J5H.js.map +0 -1
  51. package/dist/chunk-RBPVEAVM.js.map +0 -1
  52. package/dist/chunk-X57MWW35.js.map +0 -1
  53. package/dist/chunk-ZLWAYQEQ.js.map +0 -1
  54. package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
  55. /package/dist/{backup-ZV4Y424B.js.map → backup-IQE4QC7Q.js.map} +0 -0
  56. /package/dist/{chunk-IOEFNHHP.js.map → chunk-COJSQBXW.js.map} +0 -0
  57. /package/dist/{export-audit-AJ7RORNK.js.map → export-audit-MT2LTMIN.js.map} +0 -0
  58. /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-NGSBJR44.js.map} +0 -0
@@ -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":"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","names":[]}
@@ -21,7 +21,7 @@ var __copyProps = (to, from, except, desc) => {
21
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
22
 
23
23
  // src/errors.ts
24
- var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError;
24
+ var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError;
25
25
  var init_errors = __esm({
26
26
  "src/errors.ts"() {
27
27
  "use strict";
@@ -48,6 +48,16 @@ var init_errors = __esm({
48
48
  this.name = "WorkerTimeoutError";
49
49
  }
50
50
  };
51
+ NoLeaderError = class extends import_core.KoraError {
52
+ constructor(operation) {
53
+ super(
54
+ `No live leader tab is answering multi-tab storage RPC for operation "${operation}"`,
55
+ "NO_LEADER",
56
+ { operation }
57
+ );
58
+ this.name = "NoLeaderError";
59
+ }
60
+ };
51
61
  }
52
62
  });
53
63
 
@@ -270,45 +280,73 @@ var SharedWorkerClientBridge = class {
270
280
  init_errors();
271
281
  var RPC_REQUEST = "kora-worker-request";
272
282
  var RPC_RESPONSE = "kora-worker-response";
283
+ var LEADER_PING = "kora-leader-ping";
284
+ var LEADER_PONG = "kora-leader-pong";
285
+ var DEFAULT_LIVENESS_PROBE_MS = 2e3;
273
286
  function isSharedWorkerStorageSupported() {
274
287
  return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
275
288
  }
276
289
  function isMultiTabStorageSupported() {
277
290
  return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
278
291
  }
279
- async function acquireTabStorageSession(dbName) {
292
+ async function acquireTabStorageSession(dbName, options) {
280
293
  const channelName = `kora-storage-${dbName}`;
294
+ const lockName = `kora-leader-${dbName}`;
281
295
  if (!isMultiTabStorageSupported()) {
282
296
  return { role: "leader", channelName };
283
297
  }
284
298
  return new Promise((resolve) => {
285
299
  let releaseHeld;
286
- void navigator.locks.request(
287
- `kora-leader-${dbName}`,
288
- { mode: "exclusive", ifAvailable: true },
289
- (lock) => {
290
- if (lock === null) {
291
- resolve({ role: "follower", channelName });
292
- return;
293
- }
294
- resolve({
295
- role: "leader",
296
- channelName,
297
- releaseLock: async () => {
298
- releaseHeld?.();
299
- }
300
- });
301
- return new Promise((release) => {
302
- releaseHeld = release;
303
- });
300
+ void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
301
+ if (lock === null) {
302
+ resolve(startFollowerSession(channelName, lockName, options));
303
+ return;
304
304
  }
305
- );
305
+ resolve({
306
+ role: "leader",
307
+ channelName,
308
+ releaseLock: async () => {
309
+ releaseHeld?.();
310
+ }
311
+ });
312
+ return new Promise((release) => {
313
+ releaseHeld = release;
314
+ });
315
+ });
306
316
  });
307
317
  }
318
+ function startFollowerSession(channelName, lockName, options) {
319
+ let promoted = false;
320
+ let releasePromotedLock;
321
+ const abort = new AbortController();
322
+ void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
323
+ promoted = true;
324
+ options?.onPromote?.();
325
+ return new Promise((release) => {
326
+ releasePromotedLock = release;
327
+ });
328
+ }).catch(() => {
329
+ });
330
+ return {
331
+ role: "follower",
332
+ channelName,
333
+ cancelPromotionWatch: () => {
334
+ if (promoted) {
335
+ releasePromotedLock?.();
336
+ } else {
337
+ abort.abort();
338
+ }
339
+ }
340
+ };
341
+ }
308
342
  function startLeaderRpcRelay(channelName, bridge) {
309
343
  const channel = new BroadcastChannel(channelName);
310
344
  const onMessage = (event) => {
311
345
  const data = event.data;
346
+ if (data?.type === LEADER_PING) {
347
+ channel.postMessage({ type: LEADER_PONG });
348
+ return;
349
+ }
312
350
  if (data?.type !== RPC_REQUEST) {
313
351
  return;
314
352
  }
@@ -344,9 +382,11 @@ var FollowerBroadcastBridge = class {
344
382
  channel;
345
383
  pending = /* @__PURE__ */ new Map();
346
384
  timeoutMs;
385
+ livenessProbeMs;
347
386
  terminated = false;
348
- constructor(channelName, timeoutMs = 3e4) {
387
+ constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
349
388
  this.timeoutMs = timeoutMs;
389
+ this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
350
390
  this.channel = new BroadcastChannel(channelName);
351
391
  this.channel.addEventListener("message", (event) => {
352
392
  const data = event.data;
@@ -360,6 +400,48 @@ var FollowerBroadcastBridge = class {
360
400
  }
361
401
  });
362
402
  }
403
+ /**
404
+ * Readiness handshake: resolves `true` as soon as a live leader answers a ping,
405
+ * or `false` if none answers within the budget. The adapter calls this before
406
+ * its first RPC so a follower created before a leader relay is live retries the
407
+ * handshake instead of firing into the void and waiting out the full timeout.
408
+ */
409
+ async waitForLeader(timeoutMs = 3e3, attempts = 3) {
410
+ const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
411
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
412
+ if (this.terminated) {
413
+ return false;
414
+ }
415
+ if (await this.pingLeader(perAttempt)) {
416
+ return true;
417
+ }
418
+ }
419
+ return false;
420
+ }
421
+ /** Sends one liveness ping and resolves whether a leader answered in time. */
422
+ pingLeader(timeoutMs) {
423
+ if (this.terminated) {
424
+ return Promise.resolve(false);
425
+ }
426
+ return new Promise((resolve) => {
427
+ const onPong = (event) => {
428
+ if (event.data?.type === LEADER_PONG) {
429
+ cleanup();
430
+ resolve(true);
431
+ }
432
+ };
433
+ const cleanup = () => {
434
+ clearTimeout(timer);
435
+ this.channel.removeEventListener("message", onPong);
436
+ };
437
+ const timer = setTimeout(() => {
438
+ cleanup();
439
+ resolve(false);
440
+ }, timeoutMs);
441
+ this.channel.addEventListener("message", onPong);
442
+ this.channel.postMessage({ type: LEADER_PING });
443
+ });
444
+ }
363
445
  async send(request) {
364
446
  if (this.terminated) {
365
447
  return {
@@ -372,19 +454,25 @@ var FollowerBroadcastBridge = class {
372
454
  const requestId = crypto.randomUUID();
373
455
  const msg = { type: RPC_REQUEST, requestId, request };
374
456
  return new Promise((resolve, reject) => {
375
- const timer = setTimeout(() => {
457
+ const settle = (fn) => {
458
+ clearTimeout(timer);
459
+ clearTimeout(probe);
376
460
  this.pending.delete(requestId);
377
- reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs));
461
+ fn();
462
+ };
463
+ const timer = setTimeout(() => {
464
+ settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
378
465
  }, this.timeoutMs);
466
+ const probe = setTimeout(() => {
467
+ void this.pingLeader(this.livenessProbeMs).then((alive) => {
468
+ if (!alive && this.pending.has(requestId)) {
469
+ settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
470
+ }
471
+ });
472
+ }, this.livenessProbeMs);
379
473
  this.pending.set(requestId, {
380
- resolve: (response) => {
381
- clearTimeout(timer);
382
- resolve(response);
383
- },
384
- reject: (error) => {
385
- clearTimeout(timer);
386
- reject(error);
387
- }
474
+ resolve: (response) => settle(() => resolve(response)),
475
+ reject: (error) => settle(() => reject(error))
388
476
  });
389
477
  this.channel.postMessage(msg);
390
478
  });
@@ -413,14 +501,19 @@ var SqliteWasmAdapter = class {
413
501
  sharedWorkerUrl;
414
502
  workerResponseTimeoutMs;
415
503
  dbName;
504
+ emitter;
416
505
  tabSession = null;
417
506
  sharedWorker = null;
507
+ /** Retained so a follower promoted to leader can re-open its own worker. */
508
+ schema = null;
509
+ promoting = false;
418
510
  constructor(options = {}) {
419
511
  this.injectedBridge = options.bridge;
420
512
  this.workerUrl = options.workerUrl;
421
513
  this.sharedWorkerUrl = options.sharedWorkerUrl;
422
514
  this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
423
515
  this.dbName = options.dbName ?? "kora-db";
516
+ this.emitter = options.emitter;
424
517
  }
425
518
  async open(schema) {
426
519
  if (this.opened) return;
@@ -432,17 +525,29 @@ var SqliteWasmAdapter = class {
432
525
  this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
433
526
  this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
434
527
  } else if (this.workerUrl) {
435
- this.tabSession = await acquireTabStorageSession(this.dbName);
528
+ this.schema = schema;
529
+ this.tabSession = await acquireTabStorageSession(this.dbName, {
530
+ onPromote: () => {
531
+ void this.promoteToLeader();
532
+ }
533
+ });
436
534
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
437
535
  if (this.tabSession.role === "leader") {
438
536
  const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
439
537
  this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
440
538
  this.bridge = workerBridge;
441
539
  } else {
442
- this.bridge = new FollowerBroadcastBridge(
540
+ const followerBridge = new FollowerBroadcastBridge(
443
541
  this.tabSession.channelName,
444
542
  this.workerResponseTimeoutMs
445
543
  );
544
+ this.bridge = followerBridge;
545
+ this.emitter?.emit({
546
+ type: "store:db-name-collision",
547
+ dbName: this.dbName,
548
+ 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.`
549
+ });
550
+ await followerBridge.waitForLeader();
446
551
  }
447
552
  } else {
448
553
  throw new AdapterError(
@@ -462,14 +567,37 @@ var SqliteWasmAdapter = class {
462
567
  dbName: this.dbName
463
568
  });
464
569
  }
570
+ this.reportStorageMode(response.data);
465
571
  this.opened = true;
466
572
  }
573
+ /**
574
+ * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
575
+ * persistence silently degraded to an in-memory database, so a data-loss
576
+ * condition is observable instead of failing silently. A persistent open (or a
577
+ * bridge that does not report a mode, e.g. the Node mock) emits nothing.
578
+ */
579
+ reportStorageMode(data) {
580
+ if (!this.emitter || typeof data !== "object" || data === null) {
581
+ return;
582
+ }
583
+ const mode = data;
584
+ if (mode.persistent === false) {
585
+ const reason = mode.fallbackReason ?? "unsupported";
586
+ this.emitter.emit({
587
+ type: "store:opfs-unavailable",
588
+ dbName: this.dbName,
589
+ reason,
590
+ message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
591
+ });
592
+ }
593
+ }
467
594
  async close() {
468
595
  if (!this.bridge) return;
469
596
  try {
470
597
  await this.sendRequest({ id: 0, type: "close" });
471
598
  } finally {
472
599
  this.tabSession?.stopRelay?.();
600
+ this.tabSession?.cancelPromotionWatch?.();
473
601
  if (this.tabSession?.releaseLock) {
474
602
  await this.tabSession.releaseLock();
475
603
  }
@@ -480,6 +608,30 @@ var SqliteWasmAdapter = class {
480
608
  this.opened = false;
481
609
  }
482
610
  }
611
+ /**
612
+ * Promotes a follower to leader after the previous leader released the storage
613
+ * lock (its tab closed or crashed). The lock is only granted once the old leader
614
+ * is gone, so opening our own worker here is safe from the OPFS single-writer
615
+ * rule. In-flight follower RPCs are rejected and left to the caller / reactive
616
+ * layer to retry against the new leader.
617
+ */
618
+ async promoteToLeader() {
619
+ if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
620
+ return;
621
+ }
622
+ this.promoting = true;
623
+ const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
624
+ const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
625
+ const previousBridge = this.bridge;
626
+ this.bridge = workerBridge;
627
+ if (this.tabSession) {
628
+ this.tabSession.role = "leader";
629
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
630
+ }
631
+ previousBridge?.terminate();
632
+ const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
633
+ await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
634
+ }
483
635
  async execute(sql, params) {
484
636
  this.guardOpen();
485
637
  const response = await this.sendRequest({ id: 0, type: "execute", sql, params });