@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13

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 (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +480 -62
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-4NUPDEMB.js +25 -0
  25. package/dist/chunk-32EDKWMI.js +876 -0
  26. package/dist/chunk-32EDKWMI.js.map +1 -0
  27. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  28. package/dist/chunk-A6GFH5IG.js +36 -0
  29. package/dist/chunk-A6GFH5IG.js.map +1 -0
  30. package/dist/chunk-DUKTWVZW.js +175 -0
  31. package/dist/chunk-DUKTWVZW.js.map +1 -0
  32. package/dist/chunk-HZTDBLDB.js +607 -0
  33. package/dist/chunk-HZTDBLDB.js.map +1 -0
  34. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  35. package/dist/chunk-J5AGXF3D.js.map +1 -0
  36. package/dist/chunk-MCQKEHOD.js +112 -0
  37. package/dist/chunk-MCQKEHOD.js.map +1 -0
  38. package/dist/chunk-P4V375FY.js +810 -0
  39. package/dist/chunk-P4V375FY.js.map +1 -0
  40. package/dist/chunk-QDUSJMFV.js +1095 -0
  41. package/dist/chunk-QDUSJMFV.js.map +1 -0
  42. package/dist/chunk-SJP2HTYZ.js +1003 -0
  43. package/dist/chunk-SJP2HTYZ.js.map +1 -0
  44. package/dist/chunk-W5F2LWG6.js +591 -0
  45. package/dist/chunk-W5F2LWG6.js.map +1 -0
  46. package/dist/chunk-Y5LRH7KI.js +5341 -0
  47. package/dist/chunk-Y5LRH7KI.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9354 -4157
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1064 -108
  52. package/dist/index.d.ts +1064 -108
  53. package/dist/index.js +249 -3092
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +580 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-FR7XIKT2.js +299 -0
  61. package/dist/restore-FR7XIKT2.js.map +1 -0
  62. package/dist/sql-builder-C4u55jKW.d.cts +608 -0
  63. package/dist/sql-builder-CGckdBDL.d.ts +608 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
  67. package/dist/store-IMWKZLWB.js +16 -0
  68. package/dist/store-IMWKZLWB.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
@@ -0,0 +1,1095 @@
1
+ import {
2
+ Mutex
3
+ } from "./chunk-TVJEC5S2.js";
4
+ import {
5
+ FollowerBroadcastBridge,
6
+ acquireTabStorageSession
7
+ } from "./chunk-W5F2LWG6.js";
8
+ import {
9
+ AdapterError,
10
+ BridgeTerminatedError,
11
+ PersistenceError,
12
+ StorageDurabilityError,
13
+ StoreNotOpenError
14
+ } from "./chunk-DUKTWVZW.js";
15
+
16
+ // src/adapters/sqlite-wasm-adapter.ts
17
+ import { generateFullDDL } from "@korajs/core";
18
+
19
+ // src/adapters/database-dump.ts
20
+ import { quoteIdent } from "@korajs/core";
21
+ function ensureSafeIdentifier(identifier) {
22
+ if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
23
+ throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
24
+ code: "UNSAFE_IDENTIFIER",
25
+ identifier
26
+ });
27
+ }
28
+ return identifier;
29
+ }
30
+ async function exportDump(query) {
31
+ const tableRows = await query(
32
+ "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
33
+ );
34
+ const tables = [];
35
+ for (const tableRow of tableRows) {
36
+ const tableName = ensureSafeIdentifier(tableRow.name);
37
+ const columns = await query(`PRAGMA table_info(${quoteIdent(tableName)})`);
38
+ const rows = await query(`SELECT * FROM ${quoteIdent(tableName)}`);
39
+ tables.push({
40
+ name: tableName,
41
+ ...typeof tableRow.sql === "string" ? { sql: tableRow.sql } : {},
42
+ columns: columns.map((column) => column.name),
43
+ rows
44
+ });
45
+ }
46
+ return { tables };
47
+ }
48
+ function restoreDumpStatements(dump, _createMissingTables = true) {
49
+ const statements = [];
50
+ for (const table of dump.tables) {
51
+ const name = quoteIdent(ensureSafeIdentifier(table.name));
52
+ const create = createTableIfNotExists(table.name, table.sql);
53
+ if (create) {
54
+ statements.push({ sql: create });
55
+ } else {
56
+ const columns = table.columns.map((column) => quoteIdent(ensureSafeIdentifier(column)));
57
+ if (columns.length === 0) continue;
58
+ statements.push({ sql: `CREATE TABLE IF NOT EXISTS ${name} (${columns.join(", ")})` });
59
+ }
60
+ statements.push({ sql: `DELETE FROM ${name}` });
61
+ for (const row of table.rows) {
62
+ const columns = table.columns.filter(
63
+ (column) => Object.prototype.hasOwnProperty.call(row, column)
64
+ );
65
+ if (columns.length === 0) continue;
66
+ const quotedColumns = columns.map((column) => quoteIdent(ensureSafeIdentifier(column))).join(", ");
67
+ statements.push({
68
+ sql: `INSERT INTO ${name} (${quotedColumns}) VALUES (${columns.map(() => "?").join(", ")})`,
69
+ params: columns.map((column) => row[column])
70
+ });
71
+ }
72
+ }
73
+ return statements;
74
+ }
75
+ function createTableIfNotExists(tableName, sql) {
76
+ if (typeof sql !== "string" || sql.includes(";")) return null;
77
+ const match = /^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("?)([A-Za-z0-9_]+)\1\s*\(/i.exec(
78
+ sql
79
+ );
80
+ if (!match || match[2] !== tableName) return null;
81
+ return `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableName)} ${sql.slice(match[0].length - 1)}`;
82
+ }
83
+
84
+ // src/adapters/sqlite-wasm-persistence.ts
85
+ var IDB_DATABASE_NAME = "kora-persistence";
86
+ var IDB_STORE_NAME = "databases";
87
+ var IDB_VERSION = 1;
88
+ var DUMP_SUFFIX = "::dump";
89
+ function isIndexedDbQuotaError(error) {
90
+ if (error instanceof DOMException && error.name === "QuotaExceededError") {
91
+ return true;
92
+ }
93
+ if (error instanceof PersistenceError && error.context?.quotaExceeded === true) {
94
+ return true;
95
+ }
96
+ if (error instanceof Error && /quota/i.test(error.message)) {
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ function rejectWithIdbError(reject, message, context, event) {
102
+ const domError = event.target?.error;
103
+ if (domError && isIndexedDbQuotaError(domError)) {
104
+ reject(
105
+ new PersistenceError(message, {
106
+ ...context,
107
+ quotaExceeded: true,
108
+ cause: domError.message
109
+ })
110
+ );
111
+ return;
112
+ }
113
+ reject(new PersistenceError(message, context));
114
+ }
115
+ function openIdb() {
116
+ return new Promise((resolve, reject) => {
117
+ const request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
118
+ request.onupgradeneeded = () => {
119
+ const db = request.result;
120
+ if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
121
+ db.createObjectStore(IDB_STORE_NAME);
122
+ }
123
+ };
124
+ request.onsuccess = () => resolve(request.result);
125
+ request.onerror = () => reject(
126
+ new PersistenceError(
127
+ `Failed to open IndexedDB: ${request.error?.message ?? "unknown error"}`,
128
+ { database: IDB_DATABASE_NAME }
129
+ )
130
+ );
131
+ });
132
+ }
133
+ async function saveToIndexedDB(dbName, data) {
134
+ const idb = await openIdb();
135
+ try {
136
+ await new Promise((resolve, reject) => {
137
+ const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
138
+ const store = tx.objectStore(IDB_STORE_NAME);
139
+ store.put(data, dbName);
140
+ tx.oncomplete = () => resolve();
141
+ tx.onerror = (event) => rejectWithIdbError(
142
+ reject,
143
+ `Failed to save database "${dbName}" to IndexedDB`,
144
+ { dbName },
145
+ event
146
+ );
147
+ });
148
+ } finally {
149
+ idb.close();
150
+ }
151
+ }
152
+ async function saveDumpToIndexedDB(dbName, dump) {
153
+ const idb = await openIdb();
154
+ try {
155
+ await new Promise((resolve, reject) => {
156
+ const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
157
+ const store = tx.objectStore(IDB_STORE_NAME);
158
+ store.put(dump, `${dbName}${DUMP_SUFFIX}`);
159
+ tx.oncomplete = () => resolve();
160
+ tx.onerror = (event) => rejectWithIdbError(
161
+ reject,
162
+ `Failed to save dump for database "${dbName}"`,
163
+ { dbName },
164
+ event
165
+ );
166
+ });
167
+ } finally {
168
+ idb.close();
169
+ }
170
+ }
171
+ async function loadFromIndexedDB(dbName) {
172
+ const idb = await openIdb();
173
+ try {
174
+ return await new Promise((resolve, reject) => {
175
+ const tx = idb.transaction(IDB_STORE_NAME, "readonly");
176
+ const store = tx.objectStore(IDB_STORE_NAME);
177
+ const request = store.get(dbName);
178
+ request.onsuccess = () => {
179
+ const result = request.result;
180
+ if (result instanceof Uint8Array) {
181
+ resolve(result);
182
+ } else if (result) {
183
+ resolve(new Uint8Array(result));
184
+ } else {
185
+ resolve(null);
186
+ }
187
+ };
188
+ request.onerror = () => reject(
189
+ new PersistenceError(`Failed to load database "${dbName}" from IndexedDB`, { dbName })
190
+ );
191
+ });
192
+ } finally {
193
+ idb.close();
194
+ }
195
+ }
196
+ async function loadDumpFromIndexedDB(dbName) {
197
+ const idb = await openIdb();
198
+ try {
199
+ return await new Promise((resolve, reject) => {
200
+ const tx = idb.transaction(IDB_STORE_NAME, "readonly");
201
+ const store = tx.objectStore(IDB_STORE_NAME);
202
+ const request = store.get(`${dbName}${DUMP_SUFFIX}`);
203
+ request.onsuccess = () => {
204
+ resolve(request.result ?? null);
205
+ };
206
+ request.onerror = () => reject(
207
+ new PersistenceError(`Failed to load dump for database "${dbName}" from IndexedDB`, {
208
+ dbName
209
+ })
210
+ );
211
+ });
212
+ } finally {
213
+ idb.close();
214
+ }
215
+ }
216
+ async function deleteSnapshotFromIndexedDB(dbName) {
217
+ const idb = await openIdb();
218
+ try {
219
+ await new Promise((resolve, reject) => {
220
+ const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
221
+ const store = tx.objectStore(IDB_STORE_NAME);
222
+ store.delete(dbName);
223
+ tx.oncomplete = () => resolve();
224
+ tx.onerror = () => reject(
225
+ new PersistenceError(`Failed to delete database snapshot "${dbName}" from IndexedDB`, {
226
+ dbName
227
+ })
228
+ );
229
+ });
230
+ } finally {
231
+ idb.close();
232
+ }
233
+ }
234
+ async function deleteFromIndexedDB(dbName) {
235
+ const idb = await openIdb();
236
+ try {
237
+ await new Promise((resolve, reject) => {
238
+ const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
239
+ const store = tx.objectStore(IDB_STORE_NAME);
240
+ store.delete(dbName);
241
+ store.delete(`${dbName}${DUMP_SUFFIX}`);
242
+ tx.oncomplete = () => resolve();
243
+ tx.onerror = () => reject(
244
+ new PersistenceError(`Failed to delete database "${dbName}" from IndexedDB`, { dbName })
245
+ );
246
+ });
247
+ } finally {
248
+ idb.close();
249
+ }
250
+ }
251
+
252
+ // src/adapters/storage-manifest.ts
253
+ var MANIFEST_DB_NAME = "kora-manifest";
254
+ var MANIFEST_STORE = "databases";
255
+ var MANIFEST_VERSION = 1;
256
+ function isManifestAvailable() {
257
+ return typeof indexedDB !== "undefined";
258
+ }
259
+ function openManifest() {
260
+ return new Promise((resolve, reject) => {
261
+ const request = indexedDB.open(MANIFEST_DB_NAME, MANIFEST_VERSION);
262
+ request.onupgradeneeded = () => {
263
+ const db = request.result;
264
+ if (!db.objectStoreNames.contains(MANIFEST_STORE)) {
265
+ db.createObjectStore(MANIFEST_STORE, { keyPath: "dbName" });
266
+ }
267
+ };
268
+ request.onsuccess = () => resolve(request.result);
269
+ request.onerror = () => reject(request.error ?? new Error("Failed to open the Kora storage manifest"));
270
+ });
271
+ }
272
+ async function withStore(mode, run) {
273
+ const db = await openManifest();
274
+ try {
275
+ return await new Promise((resolve, reject) => {
276
+ const tx = db.transaction(MANIFEST_STORE, mode);
277
+ const request = run(tx.objectStore(MANIFEST_STORE));
278
+ let result;
279
+ if (request) {
280
+ request.onsuccess = () => {
281
+ result = request.result;
282
+ };
283
+ }
284
+ tx.oncomplete = () => resolve(result);
285
+ tx.onerror = () => reject(tx.error ?? new Error("Kora storage manifest transaction failed"));
286
+ tx.onabort = () => reject(tx.error ?? new Error("Kora storage manifest transaction aborted"));
287
+ });
288
+ } finally {
289
+ db.close();
290
+ }
291
+ }
292
+ function isRecord(value) {
293
+ if (typeof value !== "object" || value === null) return false;
294
+ const record = value;
295
+ return typeof record.dbName === "string" && (record.backend === "opfs" || record.backend === "indexeddb");
296
+ }
297
+ async function readManifestRecord(dbName) {
298
+ if (!isManifestAvailable()) return null;
299
+ const value = await withStore("readonly", (store) => store.get(dbName));
300
+ return isRecord(value) ? value : null;
301
+ }
302
+ async function listManifestRecords() {
303
+ if (!isManifestAvailable()) return [];
304
+ const values = await withStore("readonly", (store) => store.getAll());
305
+ return (values ?? []).filter(isRecord).sort((a, b) => a.dbName < b.dbName ? -1 : 1);
306
+ }
307
+ async function recordDatabase(entry) {
308
+ if (!isManifestAvailable()) return;
309
+ const existing = await readManifestRecord(entry.dbName);
310
+ const now = Date.now();
311
+ const record = {
312
+ dbName: entry.dbName,
313
+ backend: entry.backend,
314
+ ...entry.poolName ? { poolName: entry.poolName } : {},
315
+ createdAt: existing?.createdAt ?? now,
316
+ lastOpenedAt: now
317
+ };
318
+ await withStore("readwrite", (store) => store.put(record));
319
+ }
320
+ async function deleteManifestRecord(dbName) {
321
+ if (!isManifestAvailable()) return;
322
+ await withStore("readwrite", (store) => store.delete(dbName));
323
+ }
324
+
325
+ // src/adapters/sqlite-wasm-adapter.ts
326
+ var warnedSharedWorkerDeprecated = false;
327
+ function createGate() {
328
+ let open = () => {
329
+ };
330
+ const promise = new Promise((resolve) => {
331
+ open = resolve;
332
+ });
333
+ return { promise, open };
334
+ }
335
+ var SqliteWasmAdapter = class {
336
+ bridge = null;
337
+ /** The dedicated worker this tab runs while it is the database's leader. */
338
+ ownWorker = null;
339
+ opened = false;
340
+ mutex = new Mutex();
341
+ injectedBridge;
342
+ workerUrl;
343
+ sharedWorkerUrl;
344
+ workerResponseTimeoutMs;
345
+ dbName;
346
+ emitter;
347
+ tabSession = null;
348
+ emitNonPersistentDiagnostic;
349
+ storageOpenState = null;
350
+ /** Retained so a follower promoted to leader can re-open its own worker. */
351
+ schema = null;
352
+ promoting = false;
353
+ closing = false;
354
+ suspended = false;
355
+ /** Requests wait here while leadership changes hands (promotion, resume). */
356
+ gate = null;
357
+ removeLifecycleListeners = null;
358
+ allowNonDurable;
359
+ deferOpenDurabilityCheck;
360
+ releaseOnFreeze;
361
+ storage;
362
+ skipBackendRecord;
363
+ /** Set when this adapter lost (or never had) durable storage; writes are refused. */
364
+ durabilityLoss = null;
365
+ onLeaderWorkerOpened;
366
+ constructor(options = {}) {
367
+ this.injectedBridge = options.bridge;
368
+ this.workerUrl = options.workerUrl;
369
+ this.sharedWorkerUrl = options.sharedWorkerUrl;
370
+ this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
371
+ this.dbName = options.dbName ?? "kora-db";
372
+ this.emitter = options.emitter;
373
+ this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
374
+ this.allowNonDurable = options.allowNonDurable ?? false;
375
+ this.deferOpenDurabilityCheck = options.deferOpenDurabilityCheck ?? false;
376
+ this.releaseOnFreeze = options.releaseOnFreeze ?? true;
377
+ this.storage = options.storage ?? "opfs";
378
+ this.skipBackendRecord = options.skipBackendRecord ?? false;
379
+ this.onLeaderWorkerOpened = options.onLeaderWorkerOpened;
380
+ }
381
+ async open(schema) {
382
+ if (this.opened) return;
383
+ const ddlStatements = generateFullDDL(schema);
384
+ if (this.injectedBridge) {
385
+ this.bridge = this.injectedBridge;
386
+ const response = await this.openCurrentBridge(ddlStatements);
387
+ this.reportStorageMode(response.data);
388
+ this.enforceDurability("open");
389
+ this.opened = true;
390
+ return;
391
+ }
392
+ if (this.workerUrl) {
393
+ this.warnSharedWorkerDeprecated();
394
+ this.schema = schema;
395
+ try {
396
+ const response = await this.attachAndOpen(ddlStatements);
397
+ this.reportStorageMode(response.data);
398
+ this.enforceDurability("open");
399
+ await this.afterLeaderOpen(response.data);
400
+ this.opened = true;
401
+ } catch (error) {
402
+ await this.teardown();
403
+ throw error;
404
+ }
405
+ return;
406
+ }
407
+ throw new AdapterError(
408
+ 'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
409
+ );
410
+ }
411
+ /**
412
+ * Join the database as leader (own worker) or follower (RPC to the leader's
413
+ * worker), then open it. The open waits, without a timeout, while another
414
+ * holder still has the storage; the worker reports that as a blocking state.
415
+ */
416
+ async attachAndOpen(ddlStatements) {
417
+ const workerUrl = this.workerUrl;
418
+ if (!workerUrl) {
419
+ throw new AdapterError("Durable SQLite WASM storage requires workerUrl.");
420
+ }
421
+ const session = await acquireTabStorageSession(this.dbName, {
422
+ onPromote: () => {
423
+ void this.promoteToLeader();
424
+ }
425
+ });
426
+ this.tabSession = session;
427
+ if (session.role === "leader") {
428
+ const { WebWorkerBridge } = await import("./sqlite-wasm-channel-MLHLKEED.js");
429
+ const worker = new WebWorkerBridge(workerUrl, this.workerResponseTimeoutMs, {
430
+ onEvent: (event) => this.onWorkerEvent(event)
431
+ });
432
+ this.ownWorker = worker;
433
+ this.bridge = worker;
434
+ const response = await this.openLeaderWorker(worker, ddlStatements);
435
+ this.installLifecycleListeners();
436
+ return response;
437
+ }
438
+ const followerBridge = new FollowerBroadcastBridge(
439
+ session.channelName,
440
+ this.workerResponseTimeoutMs
441
+ );
442
+ this.bridge = followerBridge;
443
+ this.emitter?.emit({
444
+ type: "store:db-name-collision",
445
+ dbName: this.dbName,
446
+ 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.`
447
+ });
448
+ await followerBridge.waitForLeader();
449
+ this.installLifecycleListeners();
450
+ return this.checkedOpen(followerBridge, ddlStatements);
451
+ }
452
+ /** Open the database in this tab's own worker and start serving followers. */
453
+ async openLeaderWorker(worker, ddlStatements) {
454
+ const hook = this.onLeaderWorkerOpened;
455
+ if (!hook) {
456
+ const pending = this.checkedOpen(worker, ddlStatements);
457
+ worker.post({ id: 0, type: "serve", channelName: this.tabSession?.channelName ?? "" });
458
+ return pending;
459
+ }
460
+ const response = await this.checkedOpen(worker, ddlStatements);
461
+ await runWorkerTransaction(worker, hook);
462
+ worker.post({ id: 0, type: "serve", channelName: this.tabSession?.channelName ?? "" });
463
+ return response;
464
+ }
465
+ /**
466
+ * Whether this runtime owns the database's worker: the storage leader in a browser,
467
+ * or any adapter with an injected bridge (tests, Node). A follower tab relays every
468
+ * request to the leader's worker (STORE-6).
469
+ */
470
+ isLeader() {
471
+ if (this.injectedBridge) return true;
472
+ return this.tabSession?.role === "leader";
473
+ }
474
+ async checkedOpen(bridge, ddlStatements) {
475
+ const response = await bridge.send(
476
+ {
477
+ id: 0,
478
+ type: "open",
479
+ ddlStatements,
480
+ dbName: this.dbName,
481
+ ...this.storage === "memory" ? { storage: "memory" } : {}
482
+ },
483
+ void 0,
484
+ { timeoutMs: Number.POSITIVE_INFINITY }
485
+ );
486
+ if (response.type === "error") {
487
+ throw new AdapterError(`Failed to open database: ${response.message}`, {
488
+ code: response.code,
489
+ dbName: this.dbName,
490
+ ...response.context ?? {}
491
+ });
492
+ }
493
+ return response;
494
+ }
495
+ async openCurrentBridge(ddlStatements) {
496
+ const bridge = this.bridge;
497
+ if (!bridge) throw new StoreNotOpenError();
498
+ return this.checkedOpen(bridge, ddlStatements);
499
+ }
500
+ onWorkerEvent(event) {
501
+ if (!this.emitter) return;
502
+ const holder = event.resource === "legacy-pool" ? "the shared OPFS pool used by older Kora versions (a tab running an older version of this app may still be open)" : "another tab or worker that is still releasing it";
503
+ if (event.kind === "storage-blocked") {
504
+ this.emitter.emit({
505
+ type: "store:storage-blocked",
506
+ dbName: this.dbName,
507
+ resource: event.resource,
508
+ state: "waiting",
509
+ message: `Database "${this.dbName}" is waiting for ${holder}. Close other tabs of this app if this persists; Kora will not fall back to non-durable storage.`
510
+ });
511
+ return;
512
+ }
513
+ this.emitter.emit({
514
+ type: "store:storage-blocked",
515
+ dbName: this.dbName,
516
+ resource: event.resource,
517
+ state: "resolved",
518
+ waitedMs: event.waitedMs,
519
+ message: `Database "${this.dbName}" obtained its storage after waiting ${event.waitedMs}ms.`
520
+ });
521
+ }
522
+ /**
523
+ * Leader-only bookkeeping after a durable open: report a legacy-pool
524
+ * migration, move an IndexedDB-held copy into OPFS explicitly (never two
525
+ * disjoint copies), and record OPFS as this database's backend.
526
+ */
527
+ async afterLeaderOpen(data) {
528
+ if (!this.ownWorker || this.skipBackendRecord || this.storageOpenState?.persistent !== true) {
529
+ return;
530
+ }
531
+ const info = typeof data === "object" && data !== null ? data : {};
532
+ if (info.migratedFromLegacy === true) {
533
+ this.emitter?.emit({
534
+ type: "store:storage-migrated",
535
+ dbName: this.dbName,
536
+ from: "legacy-opfs-pool",
537
+ to: "opfs",
538
+ message: `Database "${this.dbName}" moved from the shared pre-1.0 OPFS pool into its own pool.`
539
+ });
540
+ }
541
+ if (!isManifestAvailable()) return;
542
+ let recordedBackend = null;
543
+ try {
544
+ recordedBackend = (await readManifestRecord(this.dbName))?.backend ?? null;
545
+ } catch (error) {
546
+ console.warn(`[kora] Could not read the storage manifest for "${this.dbName}":`, error);
547
+ }
548
+ if (recordedBackend === "indexeddb") {
549
+ const dump = await loadDumpFromIndexedDB(this.dbName);
550
+ if (dump) {
551
+ await this.applyDumpThroughWorker(this.ownWorker, dump);
552
+ }
553
+ this.emitter?.emit({
554
+ type: "store:storage-migrated",
555
+ dbName: this.dbName,
556
+ from: "indexeddb",
557
+ to: "opfs",
558
+ message: `Database "${this.dbName}" moved from IndexedDB into OPFS.`
559
+ });
560
+ }
561
+ try {
562
+ await recordDatabase({
563
+ dbName: this.dbName,
564
+ backend: "opfs",
565
+ ...this.storageOpenState.poolName ? { poolName: this.storageOpenState.poolName } : {}
566
+ });
567
+ if (recordedBackend === "indexeddb") {
568
+ await deleteFromIndexedDB(this.dbName);
569
+ }
570
+ } catch (error) {
571
+ console.warn(`[kora] Could not update the storage manifest for "${this.dbName}":`, error);
572
+ }
573
+ }
574
+ async applyDumpThroughWorker(worker, dump) {
575
+ const check = async (request, what) => {
576
+ const response = await worker.send(request);
577
+ if (response.type === "error") {
578
+ throw new AdapterError(`${what} failed: ${response.message}`, { dbName: this.dbName });
579
+ }
580
+ };
581
+ await check({ id: 0, type: "begin" }, "BEGIN IndexedDB import");
582
+ try {
583
+ for (const statement of restoreDumpStatements(dump)) {
584
+ await check(
585
+ { id: 0, type: "execute", sql: statement.sql, params: statement.params },
586
+ "IndexedDB import"
587
+ );
588
+ }
589
+ await check({ id: 0, type: "commit" }, "COMMIT IndexedDB import");
590
+ } catch (error) {
591
+ await worker.send({ id: 0, type: "rollback" }).catch(() => void 0);
592
+ throw error;
593
+ }
594
+ }
595
+ /**
596
+ * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
597
+ * persistence silently degraded to an in-memory database, so a data-loss
598
+ * condition is observable instead of failing silently. A persistent open (or a
599
+ * bridge that does not report a mode, e.g. the Node mock) emits nothing.
600
+ */
601
+ reportStorageMode(data) {
602
+ this.storageOpenState = parseStorageOpenState(data);
603
+ if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
604
+ return;
605
+ }
606
+ if (this.storageOpenState.persistent === false && this.storage === "opfs") {
607
+ const reason = this.storageOpenState.fallbackReason ?? "unsupported";
608
+ this.emitter.emit({
609
+ type: "store:opfs-unavailable",
610
+ dbName: this.dbName,
611
+ reason,
612
+ message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
613
+ });
614
+ }
615
+ }
616
+ getStorageOpenState() {
617
+ return this.storageOpenState;
618
+ }
619
+ /**
620
+ * Durable or loud, never silent (NEW-STORE-6). When the open or a promotion
621
+ * ended on non-persistent storage and the app did not opt into that, emit the
622
+ * blocking `store:durability-lost` event and refuse every later write. A
623
+ * bridge that reports no mode (the Node mock) is not judged.
624
+ */
625
+ enforceDurability(phase, failure) {
626
+ if (this.allowNonDurable) return;
627
+ if (phase === "open" && this.deferOpenDurabilityCheck && !failure) return;
628
+ let reason;
629
+ if (failure) {
630
+ reason = failure.reason;
631
+ } else if (this.storageOpenState?.persistent === false) {
632
+ reason = this.storageOpenState.fallbackReason ?? "unsupported";
633
+ } else {
634
+ this.durabilityLoss = null;
635
+ return;
636
+ }
637
+ this.durabilityLoss = { phase, reason };
638
+ this.emitter?.emit({
639
+ type: "store:durability-lost",
640
+ dbName: this.dbName,
641
+ phase,
642
+ reason,
643
+ message: `Database "${this.dbName}" has no durable storage (${reason} during ${phase}); writes are refused until the app reloads with durable storage.${failure ? ` ${failure.message}` : ""}`
644
+ });
645
+ }
646
+ guardDurableWrite() {
647
+ if (this.durabilityLoss) {
648
+ throw new StorageDurabilityError(
649
+ this.dbName,
650
+ this.durabilityLoss.phase,
651
+ this.durabilityLoss.reason
652
+ );
653
+ }
654
+ }
655
+ warnSharedWorkerDeprecated() {
656
+ if (!this.sharedWorkerUrl || warnedSharedWorkerDeprecated) {
657
+ return;
658
+ }
659
+ warnedSharedWorkerDeprecated = true;
660
+ console.warn(
661
+ "[kora] sharedWorkerUrl is deprecated and ignored: SharedWorker-hosted SQLite cannot use OPFS and is never durable. Kora uses the durable dedicated-worker leader/follower path for multi-tab storage."
662
+ );
663
+ }
664
+ /**
665
+ * Close the database. A leader closes the database file and releases the OPFS
666
+ * pool inside its worker, terminates the worker, and only then releases the
667
+ * tab leader lock, so the next leader never races a still-running worker for
668
+ * the file handles (NEW-STORE-10). A follower just leaves.
669
+ */
670
+ async close() {
671
+ if (!this.bridge && !this.tabSession) return;
672
+ this.closing = true;
673
+ try {
674
+ if (this.injectedBridge && this.bridge) {
675
+ await this.bridge.send({ id: 0, type: "close" });
676
+ } else if (this.ownWorker && !this.ownWorker.isTerminated()) {
677
+ const response = await this.ownWorker.send({ id: 0, type: "close" }, void 0, { timeoutMs: 5e3 }).catch((error) => ({
678
+ id: 0,
679
+ type: "error",
680
+ message: error instanceof Error ? error.message : String(error),
681
+ code: "CLOSE_FAILED"
682
+ }));
683
+ if (response.type === "error") {
684
+ console.warn(`[kora] Closing "${this.dbName}" in its worker failed:`, response.message);
685
+ }
686
+ }
687
+ } finally {
688
+ await this.teardown();
689
+ this.closing = false;
690
+ }
691
+ }
692
+ /**
693
+ * Release everything this adapter holds, in ownership order: lifecycle
694
+ * listeners, then the worker (which holds the OPFS pool and its Web Lock),
695
+ * then the tab leader lock or queued promotion request.
696
+ */
697
+ async teardown() {
698
+ this.removeLifecycleListeners?.();
699
+ this.removeLifecycleListeners = null;
700
+ const session = this.tabSession;
701
+ this.tabSession = null;
702
+ session?.stopRelay?.();
703
+ this.bridge?.terminate();
704
+ this.bridge = null;
705
+ this.ownWorker = null;
706
+ if (session) {
707
+ if (session.releaseLock) {
708
+ await session.releaseLock();
709
+ }
710
+ session.cancelPromotionWatch?.();
711
+ }
712
+ this.opened = false;
713
+ this.suspended = false;
714
+ const gate = this.gate;
715
+ this.gate = null;
716
+ gate?.open();
717
+ }
718
+ /**
719
+ * Promotes a follower to leader after the previous leader released the storage
720
+ * lock (its tab closed, crashed, or suspended on freeze). The new worker waits
721
+ * on the pool's Web Lock and handles, so it never races the previous worker.
722
+ * Requests issued during the hand-off wait for it; requests that were in
723
+ * flight to the old leader fail with a retriable `BridgeTerminatedError`.
724
+ */
725
+ async promoteToLeader() {
726
+ if (this.promoting || this.closing || !this.opened || this.workerUrl === void 0 || this.schema === null) {
727
+ return;
728
+ }
729
+ this.promoting = true;
730
+ const gate = createGate();
731
+ this.gate = gate;
732
+ try {
733
+ const { WebWorkerBridge } = await import("./sqlite-wasm-channel-MLHLKEED.js");
734
+ if (this.closing || !this.opened) return;
735
+ const worker = new WebWorkerBridge(this.workerUrl, this.workerResponseTimeoutMs, {
736
+ onEvent: (event) => this.onWorkerEvent(event)
737
+ });
738
+ const previousBridge = this.bridge;
739
+ this.bridge = worker;
740
+ this.ownWorker = worker;
741
+ if (this.tabSession) {
742
+ this.tabSession.role = "leader";
743
+ }
744
+ previousBridge?.terminate();
745
+ const ddlStatements = generateFullDDL(this.schema);
746
+ let response;
747
+ try {
748
+ response = await this.openLeaderWorker(worker, ddlStatements);
749
+ } catch (error) {
750
+ this.storageOpenState = { persistent: false, mode: "memory" };
751
+ this.enforceDurability("promotion", {
752
+ reason: "open-failed",
753
+ message: error instanceof Error ? error.message : String(error)
754
+ });
755
+ return;
756
+ }
757
+ this.storageOpenState = parseStorageOpenState(response.data) ?? this.storageOpenState;
758
+ this.enforceDurability("promotion");
759
+ try {
760
+ await this.afterLeaderOpen(response.data);
761
+ } catch (error) {
762
+ this.enforceDurability("promotion", {
763
+ reason: "open-failed",
764
+ message: error instanceof Error ? error.message : String(error)
765
+ });
766
+ }
767
+ } finally {
768
+ this.promoting = false;
769
+ if (this.gate === gate) this.gate = null;
770
+ gate.open();
771
+ }
772
+ }
773
+ /**
774
+ * Page Lifecycle: a frozen or bfcached page cannot run, so it must not keep
775
+ * other tabs waiting on its storage. Leaders terminate their worker (freeing
776
+ * the OPFS pool) and then release the leader lock, so a visible tab is
777
+ * promoted; followers withdraw their queued lock request so a frozen tab is
778
+ * never granted leadership it cannot use. Never uses Web Locks `steal`.
779
+ */
780
+ installLifecycleListeners() {
781
+ if (!this.releaseOnFreeze || this.removeLifecycleListeners) return;
782
+ if (typeof addEventListener !== "function" || typeof document === "undefined") return;
783
+ const onFreeze = () => this.suspend();
784
+ const onPageHide = () => this.suspend();
785
+ const onResume = () => {
786
+ void this.resume();
787
+ };
788
+ const onPageShow = (event) => {
789
+ if (event.persisted) void this.resume();
790
+ };
791
+ document.addEventListener("freeze", onFreeze);
792
+ document.addEventListener("resume", onResume);
793
+ addEventListener("pagehide", onPageHide);
794
+ addEventListener("pageshow", onPageShow);
795
+ this.removeLifecycleListeners = () => {
796
+ document.removeEventListener("freeze", onFreeze);
797
+ document.removeEventListener("resume", onResume);
798
+ removeEventListener("pagehide", onPageHide);
799
+ removeEventListener("pageshow", onPageShow);
800
+ };
801
+ }
802
+ /** Synchronous by necessity: a `freeze` handler gets no further turns. */
803
+ suspend() {
804
+ if (!this.opened || this.suspended || this.closing || this.promoting) return;
805
+ this.suspended = true;
806
+ this.gate = createGate();
807
+ const session = this.tabSession;
808
+ this.tabSession = null;
809
+ this.bridge?.terminate();
810
+ this.bridge = null;
811
+ this.ownWorker = null;
812
+ if (session) {
813
+ void session.releaseLock?.();
814
+ session.cancelPromotionWatch?.();
815
+ }
816
+ }
817
+ async resume() {
818
+ if (!this.suspended || this.closing || this.schema === null) return;
819
+ const gate = this.gate;
820
+ try {
821
+ const response = await this.attachAndOpen(generateFullDDL(this.schema));
822
+ this.reportStorageMode(response.data);
823
+ this.enforceDurability("promotion");
824
+ await this.afterLeaderOpen(response.data);
825
+ } catch (error) {
826
+ this.enforceDurability("promotion", {
827
+ reason: "open-failed",
828
+ message: error instanceof Error ? error.message : String(error)
829
+ });
830
+ } finally {
831
+ this.suspended = false;
832
+ if (this.gate === gate) this.gate = null;
833
+ gate?.open();
834
+ }
835
+ }
836
+ /**
837
+ * Non-transactional write. Takes the same mutex as {@link transaction}, so it
838
+ * never lands inside this tab's open transaction and is never rolled back
839
+ * with it (STORE-8). Inside a transaction callback, use the `tx` handle.
840
+ */
841
+ async execute(sql, params, options) {
842
+ this.guardOpen();
843
+ this.guardDurableWrite();
844
+ const release = await this.mutex.acquire();
845
+ try {
846
+ const response = await this.sendRequest(
847
+ {
848
+ id: 0,
849
+ type: "execute",
850
+ sql,
851
+ params,
852
+ ...options?.requestId ? { requestId: options.requestId } : {}
853
+ },
854
+ options?.signal ? { signal: options.signal } : void 0
855
+ );
856
+ if (response.type === "error") {
857
+ throw new AdapterError(`Execute failed: ${response.message}`, { sql, params });
858
+ }
859
+ } finally {
860
+ release();
861
+ }
862
+ }
863
+ /**
864
+ * Non-transactional read. Waits for this tab's open transaction to finish, so
865
+ * it never observes uncommitted (possibly rolled-back) rows (STORE-8).
866
+ */
867
+ async query(sql, params, options) {
868
+ this.guardOpen();
869
+ const release = await this.mutex.acquire();
870
+ try {
871
+ const response = await this.sendRequest(
872
+ {
873
+ id: 0,
874
+ type: "query",
875
+ sql,
876
+ params,
877
+ ...options?.requestId ? { requestId: options.requestId } : {}
878
+ },
879
+ options?.signal ? { signal: options.signal } : void 0
880
+ );
881
+ if (response.type === "error") {
882
+ throw new AdapterError(`Query failed: ${response.message}`, { sql, params });
883
+ }
884
+ return response.data ?? [];
885
+ } finally {
886
+ release();
887
+ }
888
+ }
889
+ async transaction(fn) {
890
+ this.guardOpen();
891
+ this.guardDurableWrite();
892
+ const release = await this.mutex.acquire();
893
+ let txBridge = null;
894
+ const sendInSpan = async (request) => {
895
+ if (txBridge === null || this.bridge !== txBridge) {
896
+ throw new BridgeTerminatedError(request.type, "storage leader changed during transaction");
897
+ }
898
+ return txBridge.send(request);
899
+ };
900
+ try {
901
+ while (this.gate) {
902
+ await this.gate.promise;
903
+ }
904
+ txBridge = this.bridge;
905
+ if (!txBridge) throw new StoreNotOpenError();
906
+ const begin = await sendInSpan({ id: 0, type: "begin" });
907
+ if (begin.type === "error") {
908
+ throw new AdapterError(`BEGIN transaction failed: ${begin.message}`);
909
+ }
910
+ const tx = {
911
+ execute: async (sql, params) => {
912
+ const response = await sendInSpan({ id: 0, type: "execute", sql, params });
913
+ if (response.type === "error") {
914
+ throw new AdapterError(`Transaction execute failed: ${response.message}`, {
915
+ sql,
916
+ params
917
+ });
918
+ }
919
+ },
920
+ query: async (sql, params) => {
921
+ const response = await sendInSpan({ id: 0, type: "query", sql, params });
922
+ if (response.type === "error") {
923
+ throw new AdapterError(`Transaction query failed: ${response.message}`, { sql, params });
924
+ }
925
+ return response.data ?? [];
926
+ }
927
+ };
928
+ await fn(tx);
929
+ const commit = await sendInSpan({ id: 0, type: "commit" });
930
+ if (commit.type === "error") {
931
+ throw new AdapterError(`COMMIT transaction failed: ${commit.message}`);
932
+ }
933
+ } catch (error) {
934
+ try {
935
+ if (txBridge && this.bridge === txBridge) {
936
+ await txBridge.send({ id: 0, type: "rollback" });
937
+ }
938
+ } catch {
939
+ }
940
+ throw error;
941
+ } finally {
942
+ release();
943
+ }
944
+ }
945
+ async migrate(from, to, migration) {
946
+ this.guardOpen();
947
+ this.guardDurableWrite();
948
+ const release = await this.mutex.acquire();
949
+ try {
950
+ await this.sendChecked({ id: 0, type: "begin" }, "BEGIN migration");
951
+ for (const sql of migration.statements) {
952
+ const response = await this.sendRequest({ id: 0, type: "execute", sql });
953
+ if (response.type === "error") {
954
+ throw new AdapterError(`Migration from v${from} to v${to} failed: ${response.message}`, {
955
+ from,
956
+ to
957
+ });
958
+ }
959
+ }
960
+ await this.sendChecked({ id: 0, type: "commit" }, "COMMIT migration");
961
+ } catch (error) {
962
+ try {
963
+ await this.sendRequest({ id: 0, type: "rollback" });
964
+ } catch {
965
+ }
966
+ if (error instanceof AdapterError) throw error;
967
+ throw new AdapterError(
968
+ `Migration from v${from} to v${to} failed: ${error.message}`,
969
+ { from, to }
970
+ );
971
+ } finally {
972
+ release();
973
+ }
974
+ }
975
+ /**
976
+ * Export the database as a Uint8Array (for IndexedDB persistence).
977
+ * Only available when the database is open.
978
+ */
979
+ async exportDatabase() {
980
+ this.guardOpen();
981
+ const release = await this.mutex.acquire();
982
+ try {
983
+ const response = await this.sendRequest({ id: 0, type: "export" });
984
+ if (response.type === "error") {
985
+ throw new AdapterError(`Export failed: ${response.message}`, {
986
+ code: response.code,
987
+ context: response.context
988
+ });
989
+ }
990
+ return response.data;
991
+ } finally {
992
+ release();
993
+ }
994
+ }
995
+ /**
996
+ * Import a serialized database snapshot.
997
+ */
998
+ async importDatabase(data) {
999
+ this.guardOpen();
1000
+ this.guardDurableWrite();
1001
+ const release = await this.mutex.acquire();
1002
+ try {
1003
+ const response = await this.sendRequest({ id: 0, type: "import", data });
1004
+ if (response.type === "error") {
1005
+ throw new AdapterError(`Import failed: ${response.message}`);
1006
+ }
1007
+ } finally {
1008
+ release();
1009
+ }
1010
+ }
1011
+ guardOpen() {
1012
+ if (!this.opened || !this.bridge && !this.gate) {
1013
+ throw new StoreNotOpenError();
1014
+ }
1015
+ }
1016
+ async sendRequest(request, options) {
1017
+ while (this.gate) {
1018
+ await this.gate.promise;
1019
+ }
1020
+ const bridge = this.bridge;
1021
+ if (!bridge) {
1022
+ throw new StoreNotOpenError();
1023
+ }
1024
+ return bridge.send(request, void 0, options);
1025
+ }
1026
+ async sendChecked(request, description) {
1027
+ const response = await this.sendRequest(request);
1028
+ if (response.type === "error") {
1029
+ throw new AdapterError(`${description} failed: ${response.message}`);
1030
+ }
1031
+ }
1032
+ };
1033
+ async function runWorkerTransaction(worker, fn) {
1034
+ const send = async (request, what) => {
1035
+ const response = await worker.send(request);
1036
+ if (response.type === "error") {
1037
+ throw new AdapterError(`${what} failed: ${response.message}`, {
1038
+ code: response.code,
1039
+ ...request.type === "execute" || request.type === "query" ? { sql: request.sql } : {}
1040
+ });
1041
+ }
1042
+ return response;
1043
+ };
1044
+ await send({ id: 0, type: "begin" }, "BEGIN");
1045
+ try {
1046
+ await fn({
1047
+ execute: async (sql, params) => {
1048
+ await send({ id: 0, type: "execute", sql, params }, "Leader open execute");
1049
+ },
1050
+ query: async (sql, params) => {
1051
+ const response = await send({ id: 0, type: "query", sql, params }, "Leader open query");
1052
+ return response.data ?? [];
1053
+ }
1054
+ });
1055
+ await send({ id: 0, type: "commit" }, "COMMIT");
1056
+ } catch (error) {
1057
+ await worker.send({ id: 0, type: "rollback" }).catch(() => void 0);
1058
+ throw error;
1059
+ }
1060
+ }
1061
+ function parseStorageOpenState(data) {
1062
+ if (typeof data !== "object" || data === null) {
1063
+ return null;
1064
+ }
1065
+ const mode = data;
1066
+ if (typeof mode.persistent !== "boolean") {
1067
+ return null;
1068
+ }
1069
+ return {
1070
+ persistent: mode.persistent,
1071
+ mode: mode.persistent ? "opfs" : "memory",
1072
+ ...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {},
1073
+ ...typeof mode.poolName === "string" ? { poolName: mode.poolName } : {},
1074
+ ...typeof mode.journalMode === "string" ? { journalMode: mode.journalMode } : {}
1075
+ };
1076
+ }
1077
+
1078
+ export {
1079
+ exportDump,
1080
+ restoreDumpStatements,
1081
+ isIndexedDbQuotaError,
1082
+ saveToIndexedDB,
1083
+ saveDumpToIndexedDB,
1084
+ loadFromIndexedDB,
1085
+ loadDumpFromIndexedDB,
1086
+ deleteSnapshotFromIndexedDB,
1087
+ deleteFromIndexedDB,
1088
+ isManifestAvailable,
1089
+ readManifestRecord,
1090
+ listManifestRecords,
1091
+ recordDatabase,
1092
+ deleteManifestRecord,
1093
+ SqliteWasmAdapter
1094
+ };
1095
+ //# sourceMappingURL=chunk-QDUSJMFV.js.map