@korajs/store 1.0.0-beta.12 → 1.0.0-beta.14

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 +856 -63
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +529 -63
  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-IHKVMUAM.js +25 -0
  25. package/dist/chunk-5AQSGMJV.js +5471 -0
  26. package/dist/chunk-5AQSGMJV.js.map +1 -0
  27. package/dist/chunk-5JMGMPUE.js +591 -0
  28. package/dist/chunk-5JMGMPUE.js.map +1 -0
  29. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  30. package/dist/chunk-A6GFH5IG.js +36 -0
  31. package/dist/chunk-A6GFH5IG.js.map +1 -0
  32. package/dist/chunk-BILSA6XZ.js +608 -0
  33. package/dist/chunk-BILSA6XZ.js.map +1 -0
  34. package/dist/chunk-BQSIL6TT.js +1095 -0
  35. package/dist/chunk-BQSIL6TT.js.map +1 -0
  36. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  37. package/dist/chunk-J5AGXF3D.js.map +1 -0
  38. package/dist/chunk-KK3GBONL.js +112 -0
  39. package/dist/chunk-KK3GBONL.js.map +1 -0
  40. package/dist/chunk-KTVNANUH.js +876 -0
  41. package/dist/chunk-KTVNANUH.js.map +1 -0
  42. package/dist/chunk-QW4UU7NG.js +1003 -0
  43. package/dist/chunk-QW4UU7NG.js.map +1 -0
  44. package/dist/chunk-SRHDJF5B.js +186 -0
  45. package/dist/chunk-SRHDJF5B.js.map +1 -0
  46. package/dist/chunk-UC2LGFPV.js +810 -0
  47. package/dist/chunk-UC2LGFPV.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9533 -4246
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1143 -110
  52. package/dist/index.d.ts +1143 -110
  53. package/dist/index.js +298 -3191
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +581 -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-MNDPR557.js +299 -0
  61. package/dist/restore-MNDPR557.js.map +1 -0
  62. package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
  63. package/dist/sql-builder-D5UMzHEo.d.ts +615 -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-QCCNBYCU.js +12 -0
  67. package/dist/store-HNGX4KTT.js +16 -0
  68. package/dist/store-HNGX4KTT.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-IHKVMUAM.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-QCCNBYCU.js.map} +0 -0
@@ -22,9 +22,374 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
 
25
- // src/adapters/sqlite-wasm-worker-core.ts
26
- var OPFS_POOL_NAME = "kora-opfs";
25
+ // src/adapters/opfs-names.ts
26
+ var LEGACY_OPFS_POOL_NAME = "kora-opfs";
27
+ var OPFS_POOL_PREFIX = "kora-opfs-";
28
+ var MAX_POOL_SLUG_LENGTH = 48;
29
+ function fnv1a32Hex(value) {
30
+ let hash = 2166136261;
31
+ for (let i = 0; i < value.length; i += 1) {
32
+ hash ^= value.charCodeAt(i);
33
+ hash = Math.imul(hash, 16777619);
34
+ }
35
+ return (hash >>> 0).toString(16).padStart(8, "0");
36
+ }
37
+ function opfsPoolNameFor(dbName) {
38
+ const sanitized = dbName.replace(/[^a-zA-Z0-9_-]/g, "_");
39
+ const slug = sanitized.slice(0, MAX_POOL_SLUG_LENGTH);
40
+ if (slug === dbName) {
41
+ return `${OPFS_POOL_PREFIX}${slug}`;
42
+ }
43
+ return `${OPFS_POOL_PREFIX}${slug}-${fnv1a32Hex(dbName)}`;
44
+ }
45
+ function opfsPoolDirectory(poolName) {
46
+ return `.${poolName}`;
47
+ }
48
+ function opfsPoolLockName(poolName) {
49
+ return `kora-opfs-pool:${poolName}`;
50
+ }
51
+ function opfsDatabaseFilename(dbName) {
52
+ const base = dbName.replace(/[^a-zA-Z0-9._-]/g, "_");
53
+ return base.endsWith(".db") ? base : `${base}.db`;
54
+ }
55
+ function opfsPoolPath(dbName) {
56
+ return `/${opfsDatabaseFilename(dbName)}`;
57
+ }
58
+
59
+ // src/errors.ts
60
+ var import_core = require("@korajs/core");
61
+ var WorkerInitError = class extends import_core.KoraError {
62
+ constructor(message, context) {
63
+ super(`Worker initialization failed: ${message}`, "WORKER_INIT_ERROR", context);
64
+ this.name = "WorkerInitError";
65
+ }
66
+ };
67
+ var BridgeTerminatedError = class extends import_core.KoraError {
68
+ constructor(operation, reason) {
69
+ super(
70
+ `Storage request "${operation}" was interrupted (${reason}); retry it against the new storage leader.`,
71
+ "BRIDGE_TERMINATED",
72
+ { operation, reason, retriable: true }
73
+ );
74
+ this.name = "BridgeTerminatedError";
75
+ }
76
+ };
77
+
78
+ // src/multi-tab/tab-storage.ts
79
+ var RPC_REQUEST = "kora-worker-request";
80
+ var RPC_RESPONSE = "kora-worker-response";
81
+ var CLIENT_LEAVE = "kora-client-leave";
82
+ var LEADER_PING = "kora-leader-ping";
83
+ var LEADER_PONG = "kora-leader-pong";
84
+ var LEADER_HEARTBEAT = "kora-leader-heartbeat";
85
+ var DEFAULT_HEARTBEAT_MS = 1e3;
86
+ var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
87
+ var DEDUP_CAPACITY = 512;
88
+ function createRequestDeduper() {
89
+ const inflight = /* @__PURE__ */ new Map();
90
+ const completed = /* @__PURE__ */ new Map();
91
+ return {
92
+ run(requestId, start) {
93
+ const done = completed.get(requestId);
94
+ if (done) return Promise.resolve(done);
95
+ if (inflight.has(requestId)) return null;
96
+ const promise = start().then((response) => {
97
+ inflight.delete(requestId);
98
+ completed.set(requestId, response);
99
+ if (completed.size > DEDUP_CAPACITY) {
100
+ const oldest = completed.keys().next().value;
101
+ if (oldest !== void 0) completed.delete(oldest);
102
+ }
103
+ return response;
104
+ });
105
+ inflight.set(requestId, promise);
106
+ return promise;
107
+ }
108
+ };
109
+ }
110
+ function serveChannel(channel, bridge, source, heartbeatMs) {
111
+ const epoch = createClientId();
112
+ const dedup = createRequestDeduper();
113
+ const beat = () => {
114
+ const message = { type: LEADER_HEARTBEAT, epoch, source };
115
+ channel.postMessage(message);
116
+ };
117
+ const respond = (requestId, response) => {
118
+ const msg = { type: RPC_RESPONSE, requestId, response };
119
+ channel.postMessage(msg);
120
+ };
121
+ const onMessage = (event) => {
122
+ const data = event.data;
123
+ if (data?.type === LEADER_PING) {
124
+ channel.postMessage({ type: LEADER_PONG });
125
+ return;
126
+ }
127
+ if (data?.type === CLIENT_LEAVE) {
128
+ if (hasReclaimClient(bridge)) {
129
+ bridge.reclaimClient(data.clientId, "client-left");
130
+ }
131
+ return;
132
+ }
133
+ if (data?.type !== RPC_REQUEST) {
134
+ return;
135
+ }
136
+ if (data.request.type === "close" || data.request.type === "destroy" || data.request.type === "serve") {
137
+ respond(data.requestId, { id: data.request.id, type: "success" });
138
+ return;
139
+ }
140
+ const pending = dedup.run(
141
+ data.requestId,
142
+ () => bridge.send(data.request, data.clientId).catch(
143
+ (error) => ({
144
+ id: data.request.id,
145
+ type: "error",
146
+ message: error instanceof Error ? error.message : "Worker RPC failed",
147
+ code: "LEADER_RPC_ERROR"
148
+ })
149
+ )
150
+ );
151
+ void pending?.then((response) => respond(data.requestId, response));
152
+ };
153
+ channel.addEventListener("message", onMessage);
154
+ const interval = heartbeatMs > 0 ? setInterval(beat, heartbeatMs) : void 0;
155
+ if (heartbeatMs > 0) beat();
156
+ return {
157
+ beat,
158
+ stop: () => {
159
+ if (interval !== void 0) clearInterval(interval);
160
+ channel.removeEventListener("message", onMessage);
161
+ channel.close();
162
+ }
163
+ };
164
+ }
165
+ function startWorkerRpcRelay(channelName, bridge, options = {}) {
166
+ return serveChannel(
167
+ new BroadcastChannel(channelName),
168
+ bridge,
169
+ "worker",
170
+ options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS
171
+ );
172
+ }
173
+ var TransactionSerializingWorkerBridge = class {
174
+ inner;
175
+ leaderClientId = createClientId();
176
+ transactionIdleTimeoutMs;
177
+ queue = [];
178
+ activeTransactionClient = null;
179
+ abortedClients = /* @__PURE__ */ new Set();
180
+ transactionIdleTimer = null;
181
+ reclaiming = false;
182
+ processing = false;
183
+ terminated = false;
184
+ constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
185
+ this.inner = inner;
186
+ this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
187
+ }
188
+ send(request, clientId = this.leaderClientId) {
189
+ if (this.terminated) {
190
+ return Promise.resolve({
191
+ id: request.id,
192
+ type: "error",
193
+ message: "Worker has been terminated",
194
+ code: "WORKER_TERMINATED"
195
+ });
196
+ }
197
+ if (this.abortedClients.has(clientId)) {
198
+ if (request.type === "begin") {
199
+ this.abortedClients.delete(clientId);
200
+ } else {
201
+ return Promise.resolve({
202
+ id: request.id,
203
+ type: "error",
204
+ message: "Previous transaction was aborted because the client stopped sending requests.",
205
+ code: "TRANSACTION_ABORTED"
206
+ });
207
+ }
208
+ }
209
+ return new Promise((resolve, reject) => {
210
+ this.queue.push({ clientId, request, resolve, reject });
211
+ void this.processQueue();
212
+ });
213
+ }
214
+ terminate() {
215
+ if (this.terminated) {
216
+ return;
217
+ }
218
+ this.terminated = true;
219
+ this.clearTransactionIdleTimer();
220
+ this.inner.terminate();
221
+ const pending = this.queue.splice(0);
222
+ for (const entry of pending) {
223
+ entry.reject(new BridgeTerminatedError(entry.request.type, "worker terminated"));
224
+ }
225
+ }
226
+ reclaimClient(clientId, reason) {
227
+ if (this.terminated) {
228
+ return;
229
+ }
230
+ void this.reclaimClientNow(clientId, reason);
231
+ }
232
+ async processQueue() {
233
+ if (this.processing || this.reclaiming) {
234
+ return;
235
+ }
236
+ this.processing = true;
237
+ try {
238
+ while (!this.terminated) {
239
+ const index = this.nextRunnableIndex();
240
+ if (index === -1) {
241
+ return;
242
+ }
243
+ const [entry] = this.queue.splice(index, 1);
244
+ if (!entry) {
245
+ return;
246
+ }
247
+ try {
248
+ const response = await this.inner.send(entry.request);
249
+ this.recordTransactionState(entry, response);
250
+ entry.resolve(response);
251
+ } catch (error) {
252
+ this.resetFailedTransaction(entry);
253
+ entry.reject(error instanceof Error ? error : new Error(String(error)));
254
+ }
255
+ }
256
+ } finally {
257
+ this.processing = false;
258
+ if (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {
259
+ void this.processQueue();
260
+ }
261
+ }
262
+ }
263
+ nextRunnableIndex() {
264
+ if (this.queue.length === 0) {
265
+ return -1;
266
+ }
267
+ if (this.activeTransactionClient === null) {
268
+ return 0;
269
+ }
270
+ return this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient);
271
+ }
272
+ recordTransactionState(entry, response) {
273
+ if (response.type === "error") {
274
+ this.resetFailedTransaction(entry);
275
+ return;
276
+ }
277
+ if (entry.request.type === "begin") {
278
+ this.activeTransactionClient = entry.clientId;
279
+ this.armTransactionIdleTimer(entry.clientId);
280
+ return;
281
+ }
282
+ if (this.activeTransactionClient === entry.clientId) {
283
+ this.armTransactionIdleTimer(entry.clientId);
284
+ }
285
+ if (this.activeTransactionClient === entry.clientId && (entry.request.type === "commit" || entry.request.type === "rollback")) {
286
+ this.activeTransactionClient = null;
287
+ this.clearTransactionIdleTimer();
288
+ }
289
+ }
290
+ resetFailedTransaction(entry) {
291
+ if (this.activeTransactionClient === entry.clientId) {
292
+ this.activeTransactionClient = null;
293
+ this.clearTransactionIdleTimer();
294
+ }
295
+ }
296
+ armTransactionIdleTimer(clientId) {
297
+ this.clearTransactionIdleTimer();
298
+ this.transactionIdleTimer = setTimeout(() => {
299
+ void this.reclaimClientNow(clientId, "transaction-idle-timeout");
300
+ }, this.transactionIdleTimeoutMs);
301
+ }
302
+ clearTransactionIdleTimer() {
303
+ if (this.transactionIdleTimer) {
304
+ clearTimeout(this.transactionIdleTimer);
305
+ this.transactionIdleTimer = null;
306
+ }
307
+ }
308
+ async reclaimClientNow(clientId, reason) {
309
+ if (this.reclaiming || this.activeTransactionClient !== clientId) {
310
+ return;
311
+ }
312
+ this.reclaiming = true;
313
+ this.clearTransactionIdleTimer();
314
+ this.activeTransactionClient = null;
315
+ this.abortedClients.add(clientId);
316
+ this.rejectQueuedClientRequests(clientId, reason);
317
+ try {
318
+ const response = await this.inner.send({ id: 0, type: "rollback" });
319
+ if (response.type === "error") {
320
+ }
321
+ } catch {
322
+ } finally {
323
+ this.reclaiming = false;
324
+ if (!this.terminated && this.nextRunnableIndex() !== -1) {
325
+ void this.processQueue();
326
+ }
327
+ }
328
+ }
329
+ rejectQueuedClientRequests(clientId, reason) {
330
+ const keep = [];
331
+ for (const entry of this.queue) {
332
+ if (entry.clientId === clientId) {
333
+ entry.resolve({
334
+ id: entry.request.id,
335
+ type: "error",
336
+ message: `Transaction client was reclaimed: ${reason}`,
337
+ code: "TRANSACTION_ABORTED"
338
+ });
339
+ } else {
340
+ keep.push(entry);
341
+ }
342
+ }
343
+ this.queue = keep;
344
+ }
345
+ };
346
+ function hasReclaimClient(bridge) {
347
+ return typeof bridge.reclaimClient === "function";
348
+ }
349
+ function createClientId() {
350
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
351
+ return crypto.randomUUID();
352
+ }
353
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
354
+ }
355
+
356
+ // src/migrations/schema-ceiling.ts
357
+ var DIRECTIVE = /^--kora:schema-ceiling (\d+)$/;
358
+ function parseSchemaCeiling(sql) {
359
+ const match = DIRECTIVE.exec(sql.trim());
360
+ return match ? Number(match[1]) : null;
361
+ }
362
+ function storedSchemaVersion(rows) {
363
+ const value = Number(rows[0]?.value ?? 0);
364
+ return Number.isFinite(value) ? value : 0;
365
+ }
366
+ function schemaAheadMessage(stored, code) {
367
+ return `KORA_SCHEMA_VERSION_AHEAD stored=${stored} code=${code}: the database was migrated by a newer build (schema version ${stored}); this build's schema version is ${code}.`;
368
+ }
369
+ var STORED_SCHEMA_VERSION_SQL = "SELECT value FROM _kora_meta WHERE key = 'schema_version'";
370
+
371
+ // src/adapters/opfs-pool-ownership.ts
372
+ var OPFS_POOL_HEADROOM = 2;
373
+ var BLOCKED_REPORT_AFTER_MS = 750;
374
+ var INSTALL_RACE_ATTEMPTS = 20;
27
375
  var OPFS_INIT_TIMEOUT_MS = 1e4;
376
+ var OPAQUE_DIR_NAME = ".opaque";
377
+ var PoolUnavailableError = class extends Error {
378
+ constructor(message, reason) {
379
+ super(message);
380
+ this.reason = reason;
381
+ this.name = "PoolUnavailableError";
382
+ }
383
+ reason;
384
+ };
385
+ function opfsSahSupported(sqlite3) {
386
+ const g = globalThis;
387
+ const fileHandle = g.FileSystemFileHandle;
388
+ return typeof sqlite3.installOpfsSAHPoolVfs === "function" && typeof g.FileSystemDirectoryHandle !== "undefined" && typeof fileHandle?.prototype?.createSyncAccessHandle === "function" && typeof navigator !== "undefined" && typeof navigator.storage?.getDirectory === "function";
389
+ }
390
+ function sleep(ms) {
391
+ return new Promise((resolve) => setTimeout(resolve, ms));
392
+ }
28
393
  async function withTimeout(promise, timeoutMs, label) {
29
394
  let timer;
30
395
  try {
@@ -32,7 +397,7 @@ async function withTimeout(promise, timeoutMs, label) {
32
397
  promise,
33
398
  new Promise((_, reject) => {
34
399
  timer = setTimeout(
35
- () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
400
+ () => reject(new PoolUnavailableError(`${label} timed out after ${timeoutMs}ms`, "timeout")),
36
401
  timeoutMs
37
402
  );
38
403
  })
@@ -41,25 +406,253 @@ async function withTimeout(promise, timeoutMs, label) {
41
406
  if (timer !== void 0) clearTimeout(timer);
42
407
  }
43
408
  }
44
- function opfsDatabaseFilename(dbName) {
45
- const base = dbName.replace(/[^a-zA-Z0-9._-]/g, "_");
46
- return base.endsWith(".db") ? base : `${base}.db`;
409
+ async function holdExclusiveLock(name, reporter) {
410
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
411
+ if (!locks || typeof locks.request !== "function") {
412
+ return () => {
413
+ };
414
+ }
415
+ const grab = (ifAvailable) => new Promise((resolve, reject) => {
416
+ locks.request(name, { mode: "exclusive", ifAvailable }, (lock) => {
417
+ if (lock === null) {
418
+ resolve(null);
419
+ return void 0;
420
+ }
421
+ return new Promise((release2) => {
422
+ resolve(() => release2());
423
+ });
424
+ }).catch(reject);
425
+ });
426
+ const immediate = await grab(true);
427
+ if (immediate) return immediate;
428
+ const start = Date.now();
429
+ reporter?.blocked();
430
+ const release = await grab(false);
431
+ if (!release) {
432
+ throw new PoolUnavailableError(`Web Lock "${name}" was not granted`, "lock-conflict");
433
+ }
434
+ reporter?.unblocked(Date.now() - start);
435
+ return release;
436
+ }
437
+ async function opfsRoot() {
438
+ return await navigator.storage.getDirectory();
47
439
  }
48
- function classifyOpfsFailure(error) {
49
- const message = error instanceof Error ? error.message : String(error);
50
- if (/timed out|timeout/i.test(message)) {
51
- return "timeout";
440
+ async function childDirectory(parent, name) {
441
+ try {
442
+ return await parent.getDirectoryHandle(name);
443
+ } catch {
444
+ return null;
52
445
  }
53
- if (/lock|already|in use|in-use|NoModificationAllowed|acquire|SyncAccessHandle|being created/i.test(
54
- message
55
- )) {
56
- return "lock-conflict";
446
+ }
447
+ async function poolDirectoryExists(poolName) {
448
+ return await childDirectory(await opfsRoot(), opfsPoolDirectory(poolName)) !== null;
449
+ }
450
+ function isHandleBusy(error) {
451
+ const name = error?.name;
452
+ return name === "NoModificationAllowedError" || name === "InvalidStateError";
453
+ }
454
+ async function poolHandlesBusy(poolName) {
455
+ const dir = await childDirectory(await opfsRoot(), opfsPoolDirectory(poolName));
456
+ if (!dir) return false;
457
+ const opaque = await childDirectory(dir, OPAQUE_DIR_NAME);
458
+ if (!opaque) return false;
459
+ for await (const [, handle] of opaque.entries()) {
460
+ if (handle.kind !== "file") continue;
461
+ try {
462
+ const access = await handle.createSyncAccessHandle();
463
+ access.close();
464
+ } catch (error) {
465
+ if (isHandleBusy(error)) return true;
466
+ throw error;
467
+ }
468
+ }
469
+ return false;
470
+ }
471
+ async function waitForPoolHandles(poolName, reporter) {
472
+ const start = Date.now();
473
+ let reported = false;
474
+ let delay = 15;
475
+ while (await poolHandlesBusy(poolName)) {
476
+ if (!reported && Date.now() - start >= BLOCKED_REPORT_AFTER_MS) {
477
+ reported = true;
478
+ reporter?.blocked();
479
+ }
480
+ await sleep(delay);
481
+ delay = Math.min(delay * 2, 500);
482
+ }
483
+ if (reported) reporter?.unblocked(Date.now() - start);
484
+ }
485
+ async function installPool(sqlite3, poolName, reporter) {
486
+ const install = sqlite3.installOpfsSAHPoolVfs;
487
+ if (!install || !opfsSahSupported(sqlite3)) {
488
+ throw new PoolUnavailableError("OPFS SyncAccessHandle pool is not supported", "unsupported");
57
489
  }
58
- return "unsupported";
490
+ let lastError = null;
491
+ for (let attempt = 0; attempt < INSTALL_RACE_ATTEMPTS; attempt += 1) {
492
+ await waitForPoolHandles(poolName, reporter);
493
+ try {
494
+ const pool = await withTimeout(
495
+ install({ name: poolName, forceReinitIfPreviouslyFailed: true }),
496
+ OPFS_INIT_TIMEOUT_MS,
497
+ "OPFS VFS install"
498
+ );
499
+ if (pool.isPaused()) {
500
+ await pool.unpauseVfs();
501
+ }
502
+ return pool;
503
+ } catch (error) {
504
+ if (error instanceof PoolUnavailableError) throw error;
505
+ lastError = error;
506
+ if (!isHandleBusy(error)) break;
507
+ await sleep(Math.min(25 * 2 ** attempt, 250));
508
+ }
509
+ }
510
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
511
+ if (isHandleBusy(lastError)) {
512
+ throw new PoolUnavailableError(
513
+ `OPFS pool "${poolName}" stayed busy: ${message}`,
514
+ "lock-conflict"
515
+ );
516
+ }
517
+ throw new PoolUnavailableError(
518
+ `OPFS pool "${poolName}" could not be installed: ${message}`,
519
+ "unsupported"
520
+ );
521
+ }
522
+ async function ensurePoolCapacity(pool) {
523
+ await pool.reserveMinimumCapacity(pool.getFileCount() + 2 + OPFS_POOL_HEADROOM);
59
524
  }
525
+ function assertDatabaseHealthy(db, label) {
526
+ const rows = db.exec({
527
+ sql: "PRAGMA quick_check",
528
+ returnValue: "resultRows",
529
+ rowMode: "array"
530
+ });
531
+ const verdict = rows?.[0]?.[0];
532
+ if (verdict !== "ok") {
533
+ throw new Error(`${label} failed integrity check: ${String(verdict)}`);
534
+ }
535
+ }
536
+ function pausePool(pool) {
537
+ if (!pool.isPaused()) {
538
+ pool.pauseVfs();
539
+ }
540
+ }
541
+ async function migrateFromLegacyPool(sqlite3, target, poolPath, reporter) {
542
+ if (!await poolDirectoryExists(LEGACY_OPFS_POOL_NAME)) {
543
+ return false;
544
+ }
545
+ const releaseLegacyLock = await holdExclusiveLock(
546
+ opfsPoolLockName(LEGACY_OPFS_POOL_NAME),
547
+ reporter
548
+ );
549
+ try {
550
+ if (!await poolDirectoryExists(LEGACY_OPFS_POOL_NAME)) {
551
+ return false;
552
+ }
553
+ const legacy = await installPool(sqlite3, LEGACY_OPFS_POOL_NAME, reporter);
554
+ try {
555
+ if (!legacy.getFileNames().includes(poolPath)) {
556
+ return false;
557
+ }
558
+ const legacyDb = new legacy.OpfsSAHPoolDb(poolPath);
559
+ try {
560
+ assertDatabaseHealthy(legacyDb, `Legacy database ${poolPath}`);
561
+ } finally {
562
+ legacyDb.close();
563
+ }
564
+ const bytes = legacy.exportFile(poolPath);
565
+ if (bytes.byteLength === 0) {
566
+ legacy.unlink(poolPath);
567
+ return false;
568
+ }
569
+ await ensurePoolCapacity(target);
570
+ await target.importDb(poolPath, bytes);
571
+ let migratedDb = null;
572
+ try {
573
+ migratedDb = new target.OpfsSAHPoolDb(poolPath);
574
+ assertDatabaseHealthy(migratedDb, `Migrated database ${poolPath}`);
575
+ } catch (error) {
576
+ migratedDb?.close();
577
+ migratedDb = null;
578
+ target.unlink(poolPath);
579
+ throw error;
580
+ }
581
+ migratedDb.close();
582
+ legacy.unlink(poolPath);
583
+ legacy.unlink(`${poolPath}-journal`);
584
+ return true;
585
+ } finally {
586
+ if (legacy.getFileCount() === 0) {
587
+ await legacy.removeVfs();
588
+ } else {
589
+ pausePool(legacy);
590
+ }
591
+ }
592
+ } finally {
593
+ releaseLegacyLock();
594
+ }
595
+ }
596
+
597
+ // src/adapters/sqlite-wasm-loader.ts
598
+ var SQLITE_INIT_TIMEOUT_MS = 6e4;
599
+ async function loadSqliteModule(init, options = {}) {
600
+ const timeoutMs = options.timeoutMs ?? SQLITE_INIT_TIMEOUT_MS;
601
+ const source = options.unhandledRejections ?? globalUnhandledRejections;
602
+ let timer;
603
+ let unsubscribe;
604
+ try {
605
+ return await Promise.race([
606
+ init(options.initOptions),
607
+ new Promise((_, reject) => {
608
+ unsubscribe = source((reason) => {
609
+ if (!isWasmLoadFailure(reason)) return;
610
+ reject(
611
+ new WorkerInitError(
612
+ `could not load the SQLite WebAssembly binary (sqlite3.wasm): ${describe(reason)}. The device may be offline before the app was cached, or the file is not served (check the URL and the application/wasm MIME type).`,
613
+ { cause: describe(reason) }
614
+ )
615
+ );
616
+ });
617
+ timer = setTimeout(
618
+ () => reject(
619
+ new WorkerInitError(`SQLite3 module init timed out after ${String(timeoutMs)}ms`)
620
+ ),
621
+ timeoutMs
622
+ );
623
+ })
624
+ ]);
625
+ } finally {
626
+ if (timer !== void 0) clearTimeout(timer);
627
+ unsubscribe?.();
628
+ }
629
+ }
630
+ function isWasmLoadFailure(reason) {
631
+ if (reason instanceof TypeError) return true;
632
+ const wasm = globalThis.WebAssembly;
633
+ if (!wasm) return false;
634
+ for (const name of ["CompileError", "LinkError", "RuntimeError"]) {
635
+ const ctor = wasm[name];
636
+ if (typeof ctor === "function" && reason instanceof ctor) return true;
637
+ }
638
+ return false;
639
+ }
640
+ function describe(reason) {
641
+ return reason instanceof Error ? reason.message : String(reason);
642
+ }
643
+ var globalUnhandledRejections = (listener) => {
644
+ const scope = globalThis;
645
+ if (typeof scope.addEventListener !== "function") return () => {
646
+ };
647
+ const handler = (event) => listener(event.reason);
648
+ scope.addEventListener("unhandledrejection", handler);
649
+ return () => scope.removeEventListener?.("unhandledrejection", handler);
650
+ };
651
+
652
+ // src/adapters/sqlite-wasm-worker-core.ts
653
+ var PROGRESS_HEARTBEAT_MS = 500;
654
+ var PROGRESS_HANDLER_OPS = 2e4;
60
655
  var sqlite3Promise = null;
61
- var opfsPoolPromise = null;
62
- var opfsFallbackReason;
63
656
  async function loadSqlite3() {
64
657
  const sqlite3InitModule = (await import("@sqlite.org/sqlite-wasm")).default;
65
658
  const wasmUrl = globalThis.__KORA_SQLITE_WASM_URL;
@@ -67,72 +660,225 @@ async function loadSqlite3() {
67
660
  locateFile: (file) => file.endsWith(".wasm") ? wasmUrl : file
68
661
  } : void 0;
69
662
  const initFn = sqlite3InitModule;
70
- return await withTimeout(initFn(initOptions), 6e4, "SQLite3 module init");
663
+ return loadSqliteModule(initFn, initOptions ? { initOptions } : {});
71
664
  }
72
665
  function getSqlite3() {
73
666
  if (!sqlite3Promise) {
74
- sqlite3Promise = loadSqlite3();
667
+ const loading = loadSqlite3();
668
+ sqlite3Promise = loading;
669
+ loading.catch(() => {
670
+ if (sqlite3Promise === loading) sqlite3Promise = null;
671
+ });
75
672
  }
76
673
  return sqlite3Promise;
77
674
  }
78
- async function installOpfsPool(sqlite3) {
79
- if (!sqlite3.installOpfsSAHPoolVfs) {
80
- opfsFallbackReason = "unsupported";
81
- return null;
675
+ function createSqliteWasmCore(options = {}) {
676
+ let db = null;
677
+ let sqlite3Api = null;
678
+ let persistent = false;
679
+ let fallbackReason;
680
+ let pool = null;
681
+ let poolName = null;
682
+ let releasePoolLock = null;
683
+ let openInfo = {
684
+ created: false,
685
+ migratedFromLegacy: false
686
+ };
687
+ let lastProgressBeat = 0;
688
+ function reporterFor(resource, name) {
689
+ return {
690
+ blocked: () => options.postEvent?.({ kind: "storage-blocked", resource, poolName: name }),
691
+ unblocked: (waitedMs) => options.postEvent?.({ kind: "storage-unblocked", resource, poolName: name, waitedMs })
692
+ };
82
693
  }
83
- try {
84
- return await withTimeout(
85
- sqlite3.installOpfsSAHPoolVfs({ name: OPFS_POOL_NAME }),
86
- OPFS_INIT_TIMEOUT_MS,
87
- "OPFS VFS install"
694
+ async function releaseStorage(mode) {
695
+ if (db) {
696
+ detachProgressHandler(db);
697
+ try {
698
+ db.close();
699
+ } finally {
700
+ db = null;
701
+ }
702
+ }
703
+ if (pool) {
704
+ if (mode === "remove") {
705
+ await pool.removeVfs();
706
+ } else {
707
+ pausePool(pool);
708
+ }
709
+ pool = null;
710
+ }
711
+ releasePoolLock?.();
712
+ releasePoolLock = null;
713
+ }
714
+ function attachProgressHandler(target) {
715
+ const register = sqlite3Api?.capi?.sqlite3_progress_handler;
716
+ if (!options.onProgress || typeof register !== "function" || !target.pointer) return;
717
+ register(
718
+ target.pointer,
719
+ PROGRESS_HANDLER_OPS,
720
+ () => {
721
+ const now = Date.now();
722
+ if (now - lastProgressBeat >= PROGRESS_HEARTBEAT_MS) {
723
+ lastProgressBeat = now;
724
+ options.onProgress?.();
725
+ }
726
+ return 0;
727
+ },
728
+ 0
88
729
  );
89
- } catch (error) {
90
- opfsFallbackReason = classifyOpfsFailure(error);
91
- return null;
92
730
  }
93
- }
94
- function getOpfsPool(sqlite3) {
95
- if (!opfsPoolPromise) {
96
- opfsPoolPromise = installOpfsPool(sqlite3);
731
+ function detachProgressHandler(target) {
732
+ const register = sqlite3Api?.capi?.sqlite3_progress_handler;
733
+ if (typeof register !== "function" || !target.pointer) return;
734
+ try {
735
+ register(target.pointer, 0, 0, 0);
736
+ } catch {
737
+ }
97
738
  }
98
- return opfsPoolPromise;
99
- }
100
- function createSqliteWasmCore() {
101
- let db = null;
102
- let sqlite3Api = null;
103
- let persistent = false;
104
- async function open(id, ddlStatements, dbName) {
739
+ async function openOpfs(sqlite3, dbName, mustExist) {
740
+ const name = opfsPoolNameFor(dbName);
741
+ const path = opfsPoolPath(dbName);
742
+ poolName = name;
743
+ releasePoolLock = await holdExclusiveLock(opfsPoolLockName(name), reporterFor("pool", name));
744
+ pool = await installPool(sqlite3, name, reporterFor("pool", name));
745
+ let migratedFromLegacy = false;
746
+ if (!pool.getFileNames().includes(path)) {
747
+ migratedFromLegacy = await migrateFromLegacyPool(
748
+ sqlite3,
749
+ pool,
750
+ path,
751
+ reporterFor("legacy-pool", name)
752
+ );
753
+ }
754
+ const existedBefore = pool.getFileNames().includes(path);
755
+ if (!existedBefore && mustExist) {
756
+ await releaseStorage(pool.getFileCount() === 0 ? "remove" : "pause");
757
+ return { id: 0, type: "error", message: `Database file ${path} not found`, code: "NOT_FOUND" };
758
+ }
759
+ await ensurePoolCapacity(pool);
105
760
  try {
106
- if (db) {
761
+ db = new pool.OpfsSAHPoolDb(path);
762
+ } catch (error) {
763
+ if (!existedBefore) pool.unlink(path);
764
+ throw error;
765
+ }
766
+ persistent = true;
767
+ openInfo = { created: !existedBefore, migratedFromLegacy };
768
+ return null;
769
+ }
770
+ async function open(id, ddlStatements, dbName, storage, mustExist) {
771
+ if (db) {
772
+ try {
107
773
  applyDdl(db, ddlStatements);
108
774
  return { id, type: "success", data: buildOpenData() };
775
+ } catch (error) {
776
+ return { id, type: "error", message: error.message, code: "INIT_ERROR" };
109
777
  }
778
+ }
779
+ let createdPath = null;
780
+ try {
110
781
  const sqlite3 = await getSqlite3();
111
782
  sqlite3Api = sqlite3;
112
- const pool = await getOpfsPool(sqlite3);
113
- if (pool) {
114
- db = new pool.OpfsSAHPoolDb(opfsDatabaseFilename(dbName ?? "kora-db"));
115
- persistent = true;
783
+ if (storage === "opfs" && opfsSahSupported(sqlite3)) {
784
+ try {
785
+ const early = await openOpfs(sqlite3, dbName, mustExist);
786
+ if (early) return { ...early, id };
787
+ if (openInfo.created) createdPath = opfsPoolPath(dbName);
788
+ } catch (error) {
789
+ if (!(error instanceof PoolUnavailableError) || error.reason === "lock-conflict") {
790
+ throw error;
791
+ }
792
+ await releaseStorage("pause");
793
+ fallbackReason = error.reason;
794
+ }
116
795
  } else {
796
+ fallbackReason = "unsupported";
797
+ }
798
+ if (!db) {
117
799
  db = new sqlite3.oo1.DB({ filename: ":memory:" });
118
800
  persistent = false;
119
801
  }
120
- db.exec({ sql: "PRAGMA journal_mode = WAL" });
121
802
  db.exec({ sql: "PRAGMA foreign_keys = ON" });
122
803
  applyDdl(db, ddlStatements);
804
+ attachProgressHandler(db);
123
805
  return { id, type: "success", data: buildOpenData() };
124
806
  } catch (error) {
125
- return { id, type: "error", message: error.message, code: "INIT_ERROR" };
807
+ const currentPool = pool;
808
+ if (db) {
809
+ try {
810
+ db.close();
811
+ } catch {
812
+ }
813
+ db = null;
814
+ }
815
+ if (createdPath && currentPool) {
816
+ try {
817
+ currentPool.unlink(createdPath);
818
+ currentPool.unlink(`${createdPath}-journal`);
819
+ } catch {
820
+ }
821
+ }
822
+ try {
823
+ await releaseStorage("pause");
824
+ } catch {
825
+ }
826
+ const reason = error instanceof PoolUnavailableError ? error.reason : void 0;
827
+ return {
828
+ id,
829
+ type: "error",
830
+ message: error.message,
831
+ code: "INIT_ERROR",
832
+ context: {
833
+ ...reason ? { reason } : {},
834
+ ...createdPath ? { removedCreatedFile: createdPath } : {}
835
+ }
836
+ };
837
+ }
838
+ }
839
+ function readJournalMode() {
840
+ if (!db) return void 0;
841
+ try {
842
+ let mode;
843
+ db.exec({
844
+ sql: "PRAGMA journal_mode",
845
+ rowMode: "object",
846
+ callback: (row) => {
847
+ if (typeof row.journal_mode === "string") mode = row.journal_mode;
848
+ }
849
+ });
850
+ return mode;
851
+ } catch {
852
+ return void 0;
126
853
  }
127
854
  }
128
855
  function buildOpenData() {
856
+ const journalMode = readJournalMode();
129
857
  return {
858
+ ...journalMode ? { journalMode } : {},
130
859
  persistent,
131
- ...persistent ? {} : { fallbackReason: opfsFallbackReason ?? "unsupported" }
860
+ ...persistent ? {} : { fallbackReason: fallbackReason ?? "unsupported" },
861
+ ...poolName && persistent ? { poolName } : {},
862
+ created: openInfo.created,
863
+ migratedFromLegacy: openInfo.migratedFromLegacy
132
864
  };
133
865
  }
134
866
  function applyDdl(target, ddlStatements) {
135
867
  for (const sql of ddlStatements) {
868
+ const ceiling = parseSchemaCeiling(sql);
869
+ if (ceiling !== null) {
870
+ const rows = [];
871
+ target.exec({
872
+ sql: STORED_SCHEMA_VERSION_SQL,
873
+ rowMode: "object",
874
+ callback: (row) => {
875
+ rows.push({ value: row.value });
876
+ }
877
+ });
878
+ const stored = storedSchemaVersion(rows);
879
+ if (stored > ceiling) throw new Error(schemaAheadMessage(stored, ceiling));
880
+ continue;
881
+ }
136
882
  if (sql.startsWith("--kora:safe-alter")) {
137
883
  try {
138
884
  target.exec({ sql: sql.replace("--kora:safe-alter\n", "") });
@@ -177,12 +923,13 @@ function createSqliteWasmCore() {
177
923
  return { id, type: "error", message: error.message, code: "QUERY_ERROR" };
178
924
  }
179
925
  }
180
- function close(id) {
181
- if (db) {
182
- db.close();
183
- db = null;
926
+ async function close(id, mode) {
927
+ try {
928
+ await releaseStorage(mode);
929
+ return { id, type: "success" };
930
+ } catch (error) {
931
+ return { id, type: "error", message: error.message, code: "CLOSE_ERROR" };
184
932
  }
185
- return { id, type: "success" };
186
933
  }
187
934
  function migrate(id, statements) {
188
935
  if (!db) {
@@ -201,10 +948,9 @@ function createSqliteWasmCore() {
201
948
  if (!db) {
202
949
  return { id, type: "error", message: "Database is not open", code: "DB_NOT_OPEN" };
203
950
  }
204
- const dbWithDeserialize = db;
205
- if (typeof dbWithDeserialize.deserialize === "function") {
951
+ if (typeof db.deserialize === "function") {
206
952
  try {
207
- dbWithDeserialize.deserialize(data);
953
+ db.deserialize(data);
208
954
  return { id, type: "success" };
209
955
  } catch (error) {
210
956
  return { id, type: "error", message: error.message, code: "IMPORT_ERROR" };
@@ -229,9 +975,17 @@ function createSqliteWasmCore() {
229
975
  try {
230
976
  switch (request.type) {
231
977
  case "open":
232
- return await open(request.id, request.ddlStatements, request.dbName);
978
+ return await open(
979
+ request.id,
980
+ request.ddlStatements,
981
+ request.dbName ?? "kora-db",
982
+ request.storage ?? "opfs",
983
+ request.mustExist === true
984
+ );
233
985
  case "close":
234
- return close(request.id);
986
+ return await close(request.id, "pause");
987
+ case "destroy":
988
+ return await close(request.id, "remove");
235
989
  case "execute":
236
990
  return execute(request.id, request.sql, request.params);
237
991
  case "query":
@@ -253,6 +1007,8 @@ function createSqliteWasmCore() {
253
1007
  message: "Export not yet supported in browser worker",
254
1008
  code: "EXPORT_NOT_SUPPORTED"
255
1009
  };
1010
+ case "serve":
1011
+ return { id: request.id, type: "success" };
256
1012
  default:
257
1013
  return {
258
1014
  id: request.id,
@@ -274,10 +1030,47 @@ function createSqliteWasmCore() {
274
1030
  }
275
1031
 
276
1032
  // src/adapters/sqlite-wasm-worker.ts
277
- var core = createSqliteWasmCore();
1033
+ var relay = null;
1034
+ var core = createSqliteWasmCore({
1035
+ postEvent: (event) => {
1036
+ const message = { id: -1, type: "event", event };
1037
+ self.postMessage(message);
1038
+ },
1039
+ // Keep followers' watchdogs fed while a long statement blocks this worker.
1040
+ onProgress: () => relay?.beat()
1041
+ });
1042
+ var serializer = new TransactionSerializingWorkerBridge({
1043
+ send: (request) => core.handle(request),
1044
+ terminate: () => {
1045
+ }
1046
+ });
278
1047
  self.onmessage = (event) => {
279
- void core.handle(event.data).then((response) => {
1048
+ const { clientId, ...request } = event.data;
1049
+ if (request.type === "serve") {
1050
+ if (!relay) {
1051
+ relay = startWorkerRpcRelay(request.channelName, serializer);
1052
+ }
1053
+ const response = { id: request.id, type: "success" };
280
1054
  self.postMessage(response);
281
- });
1055
+ return;
1056
+ }
1057
+ void serializer.send(request, clientId).then(
1058
+ (response) => {
1059
+ if (request.type === "close" || request.type === "destroy") {
1060
+ relay?.stop();
1061
+ relay = null;
1062
+ }
1063
+ self.postMessage(response);
1064
+ },
1065
+ (error) => {
1066
+ const response = {
1067
+ id: request.id,
1068
+ type: "error",
1069
+ message: error instanceof Error ? error.message : String(error),
1070
+ code: "WORKER_ERROR"
1071
+ };
1072
+ self.postMessage(response);
1073
+ }
1074
+ );
282
1075
  };
283
1076
  //# sourceMappingURL=sqlite-wasm-worker.cjs.map