@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.
- package/README.md +29 -37
- package/dist/adapters/better-sqlite3.cjs +90 -4
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +21 -12
- package/dist/adapters/better-sqlite3.d.ts +21 -12
- package/dist/adapters/better-sqlite3.js +69 -5
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +1316 -610
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +29 -3
- package/dist/adapters/indexeddb.d.ts +29 -3
- package/dist/adapters/indexeddb.js +247 -221
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +856 -63
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +529 -63
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +1032 -344
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +239 -18
- package/dist/adapters/sqlite-wasm.d.ts +239 -18
- package/dist/adapters/sqlite-wasm.js +111 -5
- package/dist/adapters/sqlite-wasm.js.map +1 -1
- package/dist/backup-IHKVMUAM.js +25 -0
- package/dist/chunk-5AQSGMJV.js +5471 -0
- package/dist/chunk-5AQSGMJV.js.map +1 -0
- package/dist/chunk-5JMGMPUE.js +591 -0
- package/dist/chunk-5JMGMPUE.js.map +1 -0
- package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
- package/dist/chunk-A6GFH5IG.js +36 -0
- package/dist/chunk-A6GFH5IG.js.map +1 -0
- package/dist/chunk-BILSA6XZ.js +608 -0
- package/dist/chunk-BILSA6XZ.js.map +1 -0
- package/dist/chunk-BQSIL6TT.js +1095 -0
- package/dist/chunk-BQSIL6TT.js.map +1 -0
- package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
- package/dist/chunk-J5AGXF3D.js.map +1 -0
- package/dist/chunk-KK3GBONL.js +112 -0
- package/dist/chunk-KK3GBONL.js.map +1 -0
- package/dist/chunk-KTVNANUH.js +876 -0
- package/dist/chunk-KTVNANUH.js.map +1 -0
- package/dist/chunk-QW4UU7NG.js +1003 -0
- package/dist/chunk-QW4UU7NG.js.map +1 -0
- package/dist/chunk-SRHDJF5B.js +186 -0
- package/dist/chunk-SRHDJF5B.js.map +1 -0
- package/dist/chunk-UC2LGFPV.js +810 -0
- package/dist/chunk-UC2LGFPV.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9533 -4246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1143 -110
- package/dist/index.d.ts +1143 -110
- package/dist/index.js +298 -3191
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +581 -264
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +15 -85
- package/dist/internal.d.ts +15 -85
- package/dist/internal.js +10 -8
- package/dist/restore-MNDPR557.js +299 -0
- package/dist/restore-MNDPR557.js.map +1 -0
- package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
- package/dist/sql-builder-D5UMzHEo.d.ts +615 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
- package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
- package/dist/store-HNGX4KTT.js +16 -0
- package/dist/store-HNGX4KTT.js.map +1 -0
- package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
- package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
- package/package.json +3 -2
- package/dist/backup-5S7K2Z35.js +0 -14
- package/dist/chunk-GSJ7EEUC.js.map +0 -1
- package/dist/chunk-HS6DBMST.js +0 -383
- package/dist/chunk-HS6DBMST.js.map +0 -1
- package/dist/chunk-RJIEKEN4.js +0 -751
- package/dist/chunk-RJIEKEN4.js.map +0 -1
- package/dist/chunk-TUR7RGEP.js +0 -84
- package/dist/chunk-TUR7RGEP.js.map +0 -1
- package/dist/chunk-VDBJQ75K.js +0 -78
- package/dist/chunk-VDBJQ75K.js.map +0 -1
- package/dist/chunk-YT7MRFCP.js +0 -687
- package/dist/chunk-YT7MRFCP.js.map +0 -1
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
- package/dist/types-BOO9EPt1.d.ts +0 -170
- package/dist/types-C_GDavG-.d.cts +0 -170
- /package/dist/{backup-5S7K2Z35.js.map → backup-IHKVMUAM.js.map} +0 -0
- /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
- /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
|
@@ -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, NoLeaderError;
|
|
24
|
+
var import_core, AdapterError, StorageDurabilityError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError, LeaderUnresponsiveError, BridgeTerminatedError, RequestAbortedError, StorageInUseError, UnsyncedDataError, PersistenceError;
|
|
25
25
|
var init_errors = __esm({
|
|
26
26
|
"src/errors.ts"() {
|
|
27
27
|
"use strict";
|
|
@@ -32,6 +32,16 @@ var init_errors = __esm({
|
|
|
32
32
|
this.name = "AdapterError";
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
+
StorageDurabilityError = class extends import_core.KoraError {
|
|
36
|
+
constructor(dbName, phase, reason) {
|
|
37
|
+
super(
|
|
38
|
+
`Database "${dbName}" has no durable storage (${reason} during ${phase}); writes are refused so they are not silently lost on reload. Close other tabs or apps using this origin's storage and reload, or set allowNonDurable: true to accept in-memory storage.`,
|
|
39
|
+
"STORAGE_DURABILITY_LOST",
|
|
40
|
+
{ dbName, phase, reason }
|
|
41
|
+
);
|
|
42
|
+
this.name = "StorageDurabilityError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
35
45
|
StoreNotOpenError = class extends import_core.KoraError {
|
|
36
46
|
constructor() {
|
|
37
47
|
super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
|
|
@@ -49,13 +59,66 @@ var init_errors = __esm({
|
|
|
49
59
|
}
|
|
50
60
|
};
|
|
51
61
|
NoLeaderError = class extends import_core.KoraError {
|
|
52
|
-
constructor(operation) {
|
|
62
|
+
constructor(operation, message = `No live leader tab is answering multi-tab storage RPC for operation "${operation}"`, code = "NO_LEADER", context = {}) {
|
|
63
|
+
super(message, code, { operation, ...context });
|
|
64
|
+
this.name = "NoLeaderError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
LeaderUnresponsiveError = class extends NoLeaderError {
|
|
68
|
+
constructor(operation, silentMs, requestId) {
|
|
53
69
|
super(
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
70
|
+
operation,
|
|
71
|
+
`The leader tab stopped responding (no heartbeat for ${silentMs}ms) during "${operation}". The request may or may not have been applied; retry it with the same requestId and the leader de-duplicates it.`,
|
|
72
|
+
"LEADER_UNRESPONSIVE",
|
|
73
|
+
{ silentMs, requestId, retriable: true }
|
|
57
74
|
);
|
|
58
|
-
this.name = "
|
|
75
|
+
this.name = "LeaderUnresponsiveError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
BridgeTerminatedError = class extends import_core.KoraError {
|
|
79
|
+
constructor(operation, reason) {
|
|
80
|
+
super(
|
|
81
|
+
`Storage request "${operation}" was interrupted (${reason}); retry it against the new storage leader.`,
|
|
82
|
+
"BRIDGE_TERMINATED",
|
|
83
|
+
{ operation, reason, retriable: true }
|
|
84
|
+
);
|
|
85
|
+
this.name = "BridgeTerminatedError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
RequestAbortedError = class extends import_core.KoraError {
|
|
89
|
+
constructor(operation, requestId) {
|
|
90
|
+
super(
|
|
91
|
+
`Storage request "${operation}" was aborted by the caller. A write may already have reached the leader; retry with the same requestId to stay idempotent.`,
|
|
92
|
+
"REQUEST_ABORTED",
|
|
93
|
+
{ operation, requestId }
|
|
94
|
+
);
|
|
95
|
+
this.name = "RequestAbortedError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
StorageInUseError = class extends import_core.KoraError {
|
|
99
|
+
constructor(dbName) {
|
|
100
|
+
super(
|
|
101
|
+
`Database "${dbName}" is open in a tab on this origin; close it before deleting it.`,
|
|
102
|
+
"STORAGE_IN_USE",
|
|
103
|
+
{ dbName }
|
|
104
|
+
);
|
|
105
|
+
this.name = "StorageInUseError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
UnsyncedDataError = class extends import_core.KoraError {
|
|
109
|
+
constructor(dbName) {
|
|
110
|
+
super(
|
|
111
|
+
`Database "${dbName}" has unsynced operations; deleting it would lose them. Sync first, or pass { force: true } to discard them deliberately.`,
|
|
112
|
+
"UNSYNCED_DATA",
|
|
113
|
+
{ dbName }
|
|
114
|
+
);
|
|
115
|
+
this.name = "UnsyncedDataError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
PersistenceError = class extends import_core.KoraError {
|
|
119
|
+
constructor(message, context) {
|
|
120
|
+
super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
|
|
121
|
+
this.name = "PersistenceError";
|
|
59
122
|
}
|
|
60
123
|
};
|
|
61
124
|
}
|
|
@@ -124,16 +187,21 @@ var init_sqlite_wasm_channel = __esm({
|
|
|
124
187
|
/**
|
|
125
188
|
* @param workerUrl - URL to the sqlite-wasm-worker script
|
|
126
189
|
* @param timeoutMs - Timeout for worker responses in milliseconds (default: 30000)
|
|
190
|
+
* @param options - Status event listener
|
|
127
191
|
*/
|
|
128
|
-
constructor(workerUrl, timeoutMs = 3e4) {
|
|
192
|
+
constructor(workerUrl, timeoutMs = 3e4, options = {}) {
|
|
129
193
|
this.timeoutMs = timeoutMs;
|
|
130
194
|
this.worker = new Worker(workerUrl, { type: "module" });
|
|
131
195
|
this.worker.onmessage = (event) => {
|
|
132
|
-
const
|
|
133
|
-
|
|
196
|
+
const message = event.data;
|
|
197
|
+
if (message.type === "event") {
|
|
198
|
+
options.onEvent?.(message.event);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const entry = this.pending.get(message.id);
|
|
134
202
|
if (entry) {
|
|
135
|
-
this.pending.delete(
|
|
136
|
-
entry.resolve(
|
|
203
|
+
this.pending.delete(message.id);
|
|
204
|
+
entry.resolve(message);
|
|
137
205
|
}
|
|
138
206
|
};
|
|
139
207
|
this.worker.onerror = (event) => {
|
|
@@ -144,7 +212,17 @@ var init_sqlite_wasm_channel = __esm({
|
|
|
144
212
|
}
|
|
145
213
|
};
|
|
146
214
|
}
|
|
147
|
-
|
|
215
|
+
/** True once {@link terminate} ran. */
|
|
216
|
+
isTerminated() {
|
|
217
|
+
return this.terminated;
|
|
218
|
+
}
|
|
219
|
+
/** Post a request without awaiting a response (fire-and-forget control messages). */
|
|
220
|
+
post(request, clientId) {
|
|
221
|
+
if (this.terminated) return;
|
|
222
|
+
const message = { ...request, id: this.nextId++, clientId };
|
|
223
|
+
this.worker.postMessage(message);
|
|
224
|
+
}
|
|
225
|
+
async send(request, clientId, options = {}) {
|
|
148
226
|
if (this.terminated) {
|
|
149
227
|
return {
|
|
150
228
|
id: request.id,
|
|
@@ -154,23 +232,40 @@ var init_sqlite_wasm_channel = __esm({
|
|
|
154
232
|
};
|
|
155
233
|
}
|
|
156
234
|
const id = this.nextId++;
|
|
157
|
-
const
|
|
235
|
+
const message = { ...request, id, clientId };
|
|
236
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
237
|
+
const signal = options.signal;
|
|
238
|
+
if (signal?.aborted) {
|
|
239
|
+
throw new RequestAbortedError(request.type, request.requestId ?? String(id));
|
|
240
|
+
}
|
|
158
241
|
return new Promise((resolve, reject) => {
|
|
159
|
-
const timer = setTimeout(() => {
|
|
242
|
+
const timer = Number.isFinite(timeoutMs) ? setTimeout(() => {
|
|
243
|
+
this.pending.delete(id);
|
|
244
|
+
cleanup();
|
|
245
|
+
reject(new WorkerTimeoutError(message.type, timeoutMs));
|
|
246
|
+
}, timeoutMs) : void 0;
|
|
247
|
+
const onAbort = () => {
|
|
160
248
|
this.pending.delete(id);
|
|
161
|
-
|
|
162
|
-
|
|
249
|
+
cleanup();
|
|
250
|
+
reject(new RequestAbortedError(request.type, request.requestId ?? String(id)));
|
|
251
|
+
};
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
254
|
+
signal?.removeEventListener("abort", onAbort);
|
|
255
|
+
};
|
|
256
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
163
257
|
this.pending.set(id, {
|
|
258
|
+
type: request.type,
|
|
164
259
|
resolve: (response) => {
|
|
165
|
-
|
|
260
|
+
cleanup();
|
|
166
261
|
resolve(response);
|
|
167
262
|
},
|
|
168
263
|
reject: (error) => {
|
|
169
|
-
|
|
264
|
+
cleanup();
|
|
170
265
|
reject(error);
|
|
171
266
|
}
|
|
172
267
|
});
|
|
173
|
-
this.worker.postMessage(
|
|
268
|
+
this.worker.postMessage(message);
|
|
174
269
|
});
|
|
175
270
|
}
|
|
176
271
|
terminate() {
|
|
@@ -179,7 +274,7 @@ var init_sqlite_wasm_channel = __esm({
|
|
|
179
274
|
this.worker.terminate();
|
|
180
275
|
for (const [id, entry] of this.pending) {
|
|
181
276
|
this.pending.delete(id);
|
|
182
|
-
entry.reject(new
|
|
277
|
+
entry.reject(new BridgeTerminatedError(entry.type, "worker terminated"));
|
|
183
278
|
}
|
|
184
279
|
}
|
|
185
280
|
};
|
|
@@ -191,14 +286,43 @@ var sqlite_wasm_exports = {};
|
|
|
191
286
|
__export(sqlite_wasm_exports, {
|
|
192
287
|
Mutex: () => Mutex,
|
|
193
288
|
SqliteWasmAdapter: () => SqliteWasmAdapter,
|
|
194
|
-
WebWorkerBridge: () => WebWorkerBridge
|
|
289
|
+
WebWorkerBridge: () => WebWorkerBridge,
|
|
290
|
+
deleteLocalDatabase: () => deleteLocalDatabase,
|
|
291
|
+
listLocalDatabases: () => listLocalDatabases,
|
|
292
|
+
opfsPoolNameFor: () => opfsPoolNameFor
|
|
195
293
|
});
|
|
196
294
|
module.exports = __toCommonJS(sqlite_wasm_exports);
|
|
197
295
|
|
|
198
296
|
// src/adapters/sqlite-wasm-adapter.ts
|
|
199
|
-
var
|
|
297
|
+
var import_core3 = require("@korajs/core");
|
|
200
298
|
init_errors();
|
|
201
299
|
|
|
300
|
+
// src/adapters/opfs-names.ts
|
|
301
|
+
var OPFS_POOL_PREFIX = "kora-opfs-";
|
|
302
|
+
var MAX_POOL_SLUG_LENGTH = 48;
|
|
303
|
+
function fnv1a32Hex(value) {
|
|
304
|
+
let hash = 2166136261;
|
|
305
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
306
|
+
hash ^= value.charCodeAt(i);
|
|
307
|
+
hash = Math.imul(hash, 16777619);
|
|
308
|
+
}
|
|
309
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
310
|
+
}
|
|
311
|
+
function opfsPoolNameFor(dbName) {
|
|
312
|
+
const sanitized = dbName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
313
|
+
const slug = sanitized.slice(0, MAX_POOL_SLUG_LENGTH);
|
|
314
|
+
if (slug === dbName) {
|
|
315
|
+
return `${OPFS_POOL_PREFIX}${slug}`;
|
|
316
|
+
}
|
|
317
|
+
return `${OPFS_POOL_PREFIX}${slug}-${fnv1a32Hex(dbName)}`;
|
|
318
|
+
}
|
|
319
|
+
function leaderLockName(dbName) {
|
|
320
|
+
return `kora-leader-${dbName}`;
|
|
321
|
+
}
|
|
322
|
+
function storageChannelName(dbName) {
|
|
323
|
+
return `kora-storage-${dbName}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
202
326
|
// src/multi-tab/tab-storage.ts
|
|
203
327
|
init_errors();
|
|
204
328
|
var RPC_REQUEST = "kora-worker-request";
|
|
@@ -206,14 +330,16 @@ var RPC_RESPONSE = "kora-worker-response";
|
|
|
206
330
|
var CLIENT_LEAVE = "kora-client-leave";
|
|
207
331
|
var LEADER_PING = "kora-leader-ping";
|
|
208
332
|
var LEADER_PONG = "kora-leader-pong";
|
|
209
|
-
var
|
|
210
|
-
var
|
|
333
|
+
var LEADER_HEARTBEAT = "kora-leader-heartbeat";
|
|
334
|
+
var DEFAULT_HEARTBEAT_MS = 1e3;
|
|
335
|
+
var MISSED_BEATS_UNRESPONSIVE = 3;
|
|
336
|
+
var MISSED_BEATS_ABSENT = 2;
|
|
211
337
|
function isMultiTabStorageSupported() {
|
|
212
338
|
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
213
339
|
}
|
|
214
340
|
async function acquireTabStorageSession(dbName, options) {
|
|
215
|
-
const channelName =
|
|
216
|
-
const lockName =
|
|
341
|
+
const channelName = storageChannelName(dbName);
|
|
342
|
+
const lockName = leaderLockName(dbName);
|
|
217
343
|
if (!isMultiTabStorageSupported()) {
|
|
218
344
|
return { role: "leader", channelName };
|
|
219
345
|
}
|
|
@@ -261,51 +387,6 @@ function startFollowerSession(channelName, lockName, options) {
|
|
|
261
387
|
}
|
|
262
388
|
};
|
|
263
389
|
}
|
|
264
|
-
function startLeaderRpcRelay(channelName, bridge) {
|
|
265
|
-
const channel = new BroadcastChannel(channelName);
|
|
266
|
-
const onMessage = (event) => {
|
|
267
|
-
const data = event.data;
|
|
268
|
-
if (data?.type === LEADER_PING) {
|
|
269
|
-
channel.postMessage({ type: LEADER_PONG });
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (data?.type === CLIENT_LEAVE) {
|
|
273
|
-
if (hasReclaimClient(bridge)) {
|
|
274
|
-
bridge.reclaimClient(data.clientId, "client-left");
|
|
275
|
-
}
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
if (data?.type !== RPC_REQUEST) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
void bridge.send(data.request, data.clientId).then((response) => {
|
|
282
|
-
const msg = {
|
|
283
|
-
type: RPC_RESPONSE,
|
|
284
|
-
requestId: data.requestId,
|
|
285
|
-
response
|
|
286
|
-
};
|
|
287
|
-
channel.postMessage(msg);
|
|
288
|
-
}).catch((error) => {
|
|
289
|
-
const message = error instanceof Error ? error.message : "Worker RPC failed";
|
|
290
|
-
const msg = {
|
|
291
|
-
type: RPC_RESPONSE,
|
|
292
|
-
requestId: data.requestId,
|
|
293
|
-
response: {
|
|
294
|
-
id: data.request.id,
|
|
295
|
-
type: "error",
|
|
296
|
-
message,
|
|
297
|
-
code: "LEADER_RPC_ERROR"
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
channel.postMessage(msg);
|
|
301
|
-
});
|
|
302
|
-
};
|
|
303
|
-
channel.addEventListener("message", onMessage);
|
|
304
|
-
return () => {
|
|
305
|
-
channel.removeEventListener("message", onMessage);
|
|
306
|
-
channel.close();
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
390
|
var FollowerBroadcastBridge = class {
|
|
310
391
|
channel;
|
|
311
392
|
clientId = createClientId();
|
|
@@ -314,17 +395,28 @@ var FollowerBroadcastBridge = class {
|
|
|
314
395
|
};
|
|
315
396
|
pending = /* @__PURE__ */ new Map();
|
|
316
397
|
timeoutMs;
|
|
317
|
-
|
|
398
|
+
heartbeatMs;
|
|
318
399
|
terminated = false;
|
|
319
|
-
|
|
400
|
+
lastHeardAt = 0;
|
|
401
|
+
heardSinceWatch = false;
|
|
402
|
+
watchdog;
|
|
403
|
+
/**
|
|
404
|
+
* @param channelName - Storage channel of the database
|
|
405
|
+
* @param timeoutMs - Hard ceiling per request (default 30s)
|
|
406
|
+
* @param heartbeatMs - Expected leader heartbeat interval, also the watchdog tick
|
|
407
|
+
*/
|
|
408
|
+
constructor(channelName, timeoutMs = 3e4, heartbeatMs = DEFAULT_HEARTBEAT_MS) {
|
|
320
409
|
this.timeoutMs = timeoutMs;
|
|
321
|
-
this.
|
|
410
|
+
this.heartbeatMs = Math.max(10, Math.min(heartbeatMs, timeoutMs));
|
|
322
411
|
this.channel = new BroadcastChannel(channelName);
|
|
323
412
|
if (typeof addEventListener === "function") {
|
|
324
413
|
addEventListener("pagehide", this.onPageHide);
|
|
325
414
|
}
|
|
326
415
|
this.channel.addEventListener("message", (event) => {
|
|
327
416
|
const data = event.data;
|
|
417
|
+
if (data?.type === LEADER_HEARTBEAT || data?.type === LEADER_PONG || data?.type === RPC_RESPONSE) {
|
|
418
|
+
this.markHeard();
|
|
419
|
+
}
|
|
328
420
|
if (data?.type !== RPC_RESPONSE) {
|
|
329
421
|
return;
|
|
330
422
|
}
|
|
@@ -339,7 +431,7 @@ var FollowerBroadcastBridge = class {
|
|
|
339
431
|
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
340
432
|
* or `false` if none answers within the budget. The adapter calls this before
|
|
341
433
|
* its first RPC so a follower created before a leader relay is live retries the
|
|
342
|
-
* handshake instead of firing into the void
|
|
434
|
+
* handshake instead of firing into the void.
|
|
343
435
|
*/
|
|
344
436
|
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
345
437
|
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
@@ -377,7 +469,7 @@ var FollowerBroadcastBridge = class {
|
|
|
377
469
|
this.channel.postMessage({ type: LEADER_PING });
|
|
378
470
|
});
|
|
379
471
|
}
|
|
380
|
-
async send(request) {
|
|
472
|
+
async send(request, _clientId, options = {}) {
|
|
381
473
|
if (this.terminated) {
|
|
382
474
|
return {
|
|
383
475
|
id: request.id,
|
|
@@ -386,34 +478,42 @@ var FollowerBroadcastBridge = class {
|
|
|
386
478
|
code: "BRIDGE_TERMINATED"
|
|
387
479
|
};
|
|
388
480
|
}
|
|
389
|
-
const requestId = createClientId();
|
|
481
|
+
const requestId = request.requestId ?? createClientId();
|
|
482
|
+
const signal = options.signal;
|
|
483
|
+
if (signal?.aborted) {
|
|
484
|
+
throw new RequestAbortedError(request.type, requestId);
|
|
485
|
+
}
|
|
390
486
|
const msg = {
|
|
391
487
|
type: RPC_REQUEST,
|
|
392
488
|
requestId,
|
|
393
489
|
clientId: this.clientId,
|
|
394
|
-
request
|
|
490
|
+
request: { ...request, requestId }
|
|
395
491
|
};
|
|
492
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
396
493
|
return new Promise((resolve, reject) => {
|
|
494
|
+
let timer;
|
|
495
|
+
const onAbort = () => {
|
|
496
|
+
settle(() => reject(new RequestAbortedError(request.type, requestId)));
|
|
497
|
+
};
|
|
397
498
|
const settle = (fn) => {
|
|
398
|
-
clearTimeout(timer);
|
|
399
|
-
|
|
499
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
500
|
+
signal?.removeEventListener("abort", onAbort);
|
|
400
501
|
this.pending.delete(requestId);
|
|
502
|
+
this.stopWatchdogIfIdle();
|
|
401
503
|
fn();
|
|
402
504
|
};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
}, this.livenessProbeMs);
|
|
505
|
+
if (Number.isFinite(timeoutMs)) {
|
|
506
|
+
timer = setTimeout(() => {
|
|
507
|
+
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, timeoutMs)));
|
|
508
|
+
}, timeoutMs);
|
|
509
|
+
}
|
|
510
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
413
511
|
this.pending.set(requestId, {
|
|
512
|
+
type: request.type,
|
|
414
513
|
resolve: (response) => settle(() => resolve(response)),
|
|
415
514
|
reject: (error) => settle(() => reject(error))
|
|
416
515
|
});
|
|
516
|
+
this.startWatchdog();
|
|
417
517
|
this.channel.postMessage(msg);
|
|
418
518
|
});
|
|
419
519
|
}
|
|
@@ -427,10 +527,51 @@ var FollowerBroadcastBridge = class {
|
|
|
427
527
|
removeEventListener("pagehide", this.onPageHide);
|
|
428
528
|
}
|
|
429
529
|
this.channel.close();
|
|
430
|
-
for (const [, entry] of this.pending) {
|
|
431
|
-
entry.reject(new
|
|
530
|
+
for (const [, entry] of [...this.pending]) {
|
|
531
|
+
entry.reject(new BridgeTerminatedError(entry.type, "follower bridge terminated"));
|
|
432
532
|
}
|
|
433
533
|
this.pending.clear();
|
|
534
|
+
this.stopWatchdog();
|
|
535
|
+
}
|
|
536
|
+
markHeard() {
|
|
537
|
+
this.lastHeardAt = Date.now();
|
|
538
|
+
this.heardSinceWatch = true;
|
|
539
|
+
}
|
|
540
|
+
startWatchdog() {
|
|
541
|
+
if (this.watchdog !== void 0) return;
|
|
542
|
+
const now = Date.now();
|
|
543
|
+
this.heardSinceWatch = now - this.lastHeardAt < this.heartbeatMs * 2;
|
|
544
|
+
if (!this.heardSinceWatch) this.lastHeardAt = now;
|
|
545
|
+
this.watchdog = setInterval(() => this.checkLiveness(), this.heartbeatMs);
|
|
546
|
+
}
|
|
547
|
+
stopWatchdogIfIdle() {
|
|
548
|
+
if (this.pending.size === 0) this.stopWatchdog();
|
|
549
|
+
}
|
|
550
|
+
stopWatchdog() {
|
|
551
|
+
if (this.watchdog !== void 0) {
|
|
552
|
+
clearInterval(this.watchdog);
|
|
553
|
+
this.watchdog = void 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
checkLiveness() {
|
|
557
|
+
if (this.pending.size === 0) {
|
|
558
|
+
this.stopWatchdog();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const silentMs = Date.now() - this.lastHeardAt;
|
|
562
|
+
const heard = this.heardSinceWatch;
|
|
563
|
+
const limit = this.heartbeatMs * (heard ? MISSED_BEATS_UNRESPONSIVE : MISSED_BEATS_ABSENT);
|
|
564
|
+
if (silentMs >= limit) {
|
|
565
|
+
for (const [requestId, entry] of [...this.pending]) {
|
|
566
|
+
entry.reject(
|
|
567
|
+
heard ? new LeaderUnresponsiveError(`follower-rpc:${entry.type}`, silentMs, requestId) : new NoLeaderError(`follower-rpc:${entry.type}`)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (silentMs >= this.heartbeatMs) {
|
|
573
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
574
|
+
}
|
|
434
575
|
}
|
|
435
576
|
leaveLeader() {
|
|
436
577
|
try {
|
|
@@ -443,194 +584,213 @@ var FollowerBroadcastBridge = class {
|
|
|
443
584
|
}
|
|
444
585
|
}
|
|
445
586
|
};
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
transactionIdleTimeoutMs;
|
|
450
|
-
queue = [];
|
|
451
|
-
activeTransactionClient = null;
|
|
452
|
-
abortedClients = /* @__PURE__ */ new Set();
|
|
453
|
-
transactionIdleTimer = null;
|
|
454
|
-
reclaiming = false;
|
|
455
|
-
processing = false;
|
|
456
|
-
terminated = false;
|
|
457
|
-
constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
|
|
458
|
-
this.inner = inner;
|
|
459
|
-
this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
|
|
587
|
+
function createClientId() {
|
|
588
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
589
|
+
return crypto.randomUUID();
|
|
460
590
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.abortedClients.delete(clientId);
|
|
473
|
-
} else {
|
|
474
|
-
return Promise.resolve({
|
|
475
|
-
id: request.id,
|
|
476
|
-
type: "error",
|
|
477
|
-
message: "Previous transaction was aborted because the client stopped sending requests.",
|
|
478
|
-
code: "TRANSACTION_ABORTED"
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return new Promise((resolve, reject) => {
|
|
483
|
-
this.queue.push({ clientId, request, resolve, reject });
|
|
484
|
-
void this.processQueue();
|
|
591
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/adapters/database-dump.ts
|
|
595
|
+
var import_core2 = require("@korajs/core");
|
|
596
|
+
init_errors();
|
|
597
|
+
function ensureSafeIdentifier(identifier) {
|
|
598
|
+
if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
|
|
599
|
+
throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
|
|
600
|
+
code: "UNSAFE_IDENTIFIER",
|
|
601
|
+
identifier
|
|
485
602
|
});
|
|
486
603
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
604
|
+
return identifier;
|
|
605
|
+
}
|
|
606
|
+
function restoreDumpStatements(dump, _createMissingTables = true) {
|
|
607
|
+
const statements = [];
|
|
608
|
+
for (const table of dump.tables) {
|
|
609
|
+
const name = (0, import_core2.quoteIdent)(ensureSafeIdentifier(table.name));
|
|
610
|
+
const create = createTableIfNotExists(table.name, table.sql);
|
|
611
|
+
if (create) {
|
|
612
|
+
statements.push({ sql: create });
|
|
613
|
+
} else {
|
|
614
|
+
const columns = table.columns.map((column) => (0, import_core2.quoteIdent)(ensureSafeIdentifier(column)));
|
|
615
|
+
if (columns.length === 0) continue;
|
|
616
|
+
statements.push({ sql: `CREATE TABLE IF NOT EXISTS ${name} (${columns.join(", ")})` });
|
|
497
617
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
618
|
+
statements.push({ sql: `DELETE FROM ${name}` });
|
|
619
|
+
for (const row of table.rows) {
|
|
620
|
+
const columns = table.columns.filter(
|
|
621
|
+
(column) => Object.prototype.hasOwnProperty.call(row, column)
|
|
622
|
+
);
|
|
623
|
+
if (columns.length === 0) continue;
|
|
624
|
+
const quotedColumns = columns.map((column) => (0, import_core2.quoteIdent)(ensureSafeIdentifier(column))).join(", ");
|
|
625
|
+
statements.push({
|
|
626
|
+
sql: `INSERT INTO ${name} (${quotedColumns}) VALUES (${columns.map(() => "?").join(", ")})`,
|
|
627
|
+
params: columns.map((column) => row[column])
|
|
628
|
+
});
|
|
502
629
|
}
|
|
503
|
-
void this.reclaimClientNow(clientId, reason);
|
|
504
630
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
void this.processQueue();
|
|
631
|
+
return statements;
|
|
632
|
+
}
|
|
633
|
+
function createTableIfNotExists(tableName, sql) {
|
|
634
|
+
if (typeof sql !== "string" || sql.includes(";")) return null;
|
|
635
|
+
const match = /^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("?)([A-Za-z0-9_]+)\1\s*\(/i.exec(
|
|
636
|
+
sql
|
|
637
|
+
);
|
|
638
|
+
if (!match || match[2] !== tableName) return null;
|
|
639
|
+
return `CREATE TABLE IF NOT EXISTS ${(0, import_core2.quoteIdent)(tableName)} ${sql.slice(match[0].length - 1)}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/adapters/sqlite-wasm-adapter.ts
|
|
643
|
+
init_sqlite_wasm_channel();
|
|
644
|
+
|
|
645
|
+
// src/adapters/sqlite-wasm-persistence.ts
|
|
646
|
+
init_errors();
|
|
647
|
+
var IDB_DATABASE_NAME = "kora-persistence";
|
|
648
|
+
var IDB_STORE_NAME = "databases";
|
|
649
|
+
var IDB_VERSION = 1;
|
|
650
|
+
var DUMP_SUFFIX = "::dump";
|
|
651
|
+
function openIdb() {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
|
|
654
|
+
request.onupgradeneeded = () => {
|
|
655
|
+
const db = request.result;
|
|
656
|
+
if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
|
|
657
|
+
db.createObjectStore(IDB_STORE_NAME);
|
|
533
658
|
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
resetFailedTransaction(entry) {
|
|
564
|
-
if (this.activeTransactionClient === entry.clientId) {
|
|
565
|
-
this.activeTransactionClient = null;
|
|
566
|
-
this.clearTransactionIdleTimer();
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
armTransactionIdleTimer(clientId) {
|
|
570
|
-
this.clearTransactionIdleTimer();
|
|
571
|
-
this.transactionIdleTimer = setTimeout(() => {
|
|
572
|
-
void this.reclaimClientNow(clientId, "transaction-idle-timeout");
|
|
573
|
-
}, this.transactionIdleTimeoutMs);
|
|
659
|
+
};
|
|
660
|
+
request.onsuccess = () => resolve(request.result);
|
|
661
|
+
request.onerror = () => reject(
|
|
662
|
+
new PersistenceError(
|
|
663
|
+
`Failed to open IndexedDB: ${request.error?.message ?? "unknown error"}`,
|
|
664
|
+
{ database: IDB_DATABASE_NAME }
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async function loadDumpFromIndexedDB(dbName) {
|
|
670
|
+
const idb = await openIdb();
|
|
671
|
+
try {
|
|
672
|
+
return await new Promise((resolve, reject) => {
|
|
673
|
+
const tx = idb.transaction(IDB_STORE_NAME, "readonly");
|
|
674
|
+
const store = tx.objectStore(IDB_STORE_NAME);
|
|
675
|
+
const request = store.get(`${dbName}${DUMP_SUFFIX}`);
|
|
676
|
+
request.onsuccess = () => {
|
|
677
|
+
resolve(request.result ?? null);
|
|
678
|
+
};
|
|
679
|
+
request.onerror = () => reject(
|
|
680
|
+
new PersistenceError(`Failed to load dump for database "${dbName}" from IndexedDB`, {
|
|
681
|
+
dbName
|
|
682
|
+
})
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
} finally {
|
|
686
|
+
idb.close();
|
|
574
687
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
688
|
+
}
|
|
689
|
+
async function deleteFromIndexedDB(dbName) {
|
|
690
|
+
const idb = await openIdb();
|
|
691
|
+
try {
|
|
692
|
+
await new Promise((resolve, reject) => {
|
|
693
|
+
const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
|
|
694
|
+
const store = tx.objectStore(IDB_STORE_NAME);
|
|
695
|
+
store.delete(dbName);
|
|
696
|
+
store.delete(`${dbName}${DUMP_SUFFIX}`);
|
|
697
|
+
tx.oncomplete = () => resolve();
|
|
698
|
+
tx.onerror = () => reject(
|
|
699
|
+
new PersistenceError(`Failed to delete database "${dbName}" from IndexedDB`, { dbName })
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
} finally {
|
|
703
|
+
idb.close();
|
|
580
704
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (!this.terminated && this.nextRunnableIndex() !== -1) {
|
|
598
|
-
void this.processQueue();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/adapters/storage-manifest.ts
|
|
708
|
+
var MANIFEST_DB_NAME = "kora-manifest";
|
|
709
|
+
var MANIFEST_STORE = "databases";
|
|
710
|
+
var MANIFEST_VERSION = 1;
|
|
711
|
+
function isManifestAvailable() {
|
|
712
|
+
return typeof indexedDB !== "undefined";
|
|
713
|
+
}
|
|
714
|
+
function openManifest() {
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
const request = indexedDB.open(MANIFEST_DB_NAME, MANIFEST_VERSION);
|
|
717
|
+
request.onupgradeneeded = () => {
|
|
718
|
+
const db = request.result;
|
|
719
|
+
if (!db.objectStoreNames.contains(MANIFEST_STORE)) {
|
|
720
|
+
db.createObjectStore(MANIFEST_STORE, { keyPath: "dbName" });
|
|
599
721
|
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
722
|
+
};
|
|
723
|
+
request.onsuccess = () => resolve(request.result);
|
|
724
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open the Kora storage manifest"));
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async function withStore(mode, run) {
|
|
728
|
+
const db = await openManifest();
|
|
729
|
+
try {
|
|
730
|
+
return await new Promise((resolve, reject) => {
|
|
731
|
+
const tx = db.transaction(MANIFEST_STORE, mode);
|
|
732
|
+
const request = run(tx.objectStore(MANIFEST_STORE));
|
|
733
|
+
let result;
|
|
734
|
+
if (request) {
|
|
735
|
+
request.onsuccess = () => {
|
|
736
|
+
result = request.result;
|
|
737
|
+
};
|
|
614
738
|
}
|
|
615
|
-
|
|
616
|
-
|
|
739
|
+
tx.oncomplete = () => resolve(result);
|
|
740
|
+
tx.onerror = () => reject(tx.error ?? new Error("Kora storage manifest transaction failed"));
|
|
741
|
+
tx.onabort = () => reject(tx.error ?? new Error("Kora storage manifest transaction aborted"));
|
|
742
|
+
});
|
|
743
|
+
} finally {
|
|
744
|
+
db.close();
|
|
617
745
|
}
|
|
618
|
-
};
|
|
619
|
-
function hasReclaimClient(bridge) {
|
|
620
|
-
return typeof bridge.reclaimClient === "function";
|
|
621
746
|
}
|
|
622
|
-
function
|
|
623
|
-
if (typeof
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
747
|
+
function isRecord(value) {
|
|
748
|
+
if (typeof value !== "object" || value === null) return false;
|
|
749
|
+
const record = value;
|
|
750
|
+
return typeof record.dbName === "string" && (record.backend === "opfs" || record.backend === "indexeddb");
|
|
751
|
+
}
|
|
752
|
+
async function readManifestRecord(dbName) {
|
|
753
|
+
if (!isManifestAvailable()) return null;
|
|
754
|
+
const value = await withStore("readonly", (store) => store.get(dbName));
|
|
755
|
+
return isRecord(value) ? value : null;
|
|
756
|
+
}
|
|
757
|
+
async function listManifestRecords() {
|
|
758
|
+
if (!isManifestAvailable()) return [];
|
|
759
|
+
const values = await withStore("readonly", (store) => store.getAll());
|
|
760
|
+
return (values ?? []).filter(isRecord).sort((a, b) => a.dbName < b.dbName ? -1 : 1);
|
|
761
|
+
}
|
|
762
|
+
async function recordDatabase(entry) {
|
|
763
|
+
if (!isManifestAvailable()) return;
|
|
764
|
+
const existing = await readManifestRecord(entry.dbName);
|
|
765
|
+
const now = Date.now();
|
|
766
|
+
const record = {
|
|
767
|
+
dbName: entry.dbName,
|
|
768
|
+
backend: entry.backend,
|
|
769
|
+
...entry.poolName ? { poolName: entry.poolName } : {},
|
|
770
|
+
createdAt: existing?.createdAt ?? now,
|
|
771
|
+
lastOpenedAt: now
|
|
772
|
+
};
|
|
773
|
+
await withStore("readwrite", (store) => store.put(record));
|
|
774
|
+
}
|
|
775
|
+
async function deleteManifestRecord(dbName) {
|
|
776
|
+
if (!isManifestAvailable()) return;
|
|
777
|
+
await withStore("readwrite", (store) => store.delete(dbName));
|
|
627
778
|
}
|
|
628
779
|
|
|
629
780
|
// src/adapters/sqlite-wasm-adapter.ts
|
|
630
|
-
init_sqlite_wasm_channel();
|
|
631
781
|
var warnedSharedWorkerDeprecated = false;
|
|
782
|
+
function createGate() {
|
|
783
|
+
let open = () => {
|
|
784
|
+
};
|
|
785
|
+
const promise = new Promise((resolve) => {
|
|
786
|
+
open = resolve;
|
|
787
|
+
});
|
|
788
|
+
return { promise, open };
|
|
789
|
+
}
|
|
632
790
|
var SqliteWasmAdapter = class {
|
|
633
791
|
bridge = null;
|
|
792
|
+
/** The dedicated worker this tab runs while it is the database's leader. */
|
|
793
|
+
ownWorker = null;
|
|
634
794
|
opened = false;
|
|
635
795
|
mutex = new Mutex();
|
|
636
796
|
injectedBridge;
|
|
@@ -645,6 +805,19 @@ var SqliteWasmAdapter = class {
|
|
|
645
805
|
/** Retained so a follower promoted to leader can re-open its own worker. */
|
|
646
806
|
schema = null;
|
|
647
807
|
promoting = false;
|
|
808
|
+
closing = false;
|
|
809
|
+
suspended = false;
|
|
810
|
+
/** Requests wait here while leadership changes hands (promotion, resume). */
|
|
811
|
+
gate = null;
|
|
812
|
+
removeLifecycleListeners = null;
|
|
813
|
+
allowNonDurable;
|
|
814
|
+
deferOpenDurabilityCheck;
|
|
815
|
+
releaseOnFreeze;
|
|
816
|
+
storage;
|
|
817
|
+
skipBackendRecord;
|
|
818
|
+
/** Set when this adapter lost (or never had) durable storage; writes are refused. */
|
|
819
|
+
durabilityLoss = null;
|
|
820
|
+
onLeaderWorkerOpened;
|
|
648
821
|
constructor(options = {}) {
|
|
649
822
|
this.injectedBridge = options.bridge;
|
|
650
823
|
this.workerUrl = options.workerUrl;
|
|
@@ -653,50 +826,72 @@ var SqliteWasmAdapter = class {
|
|
|
653
826
|
this.dbName = options.dbName ?? "kora-db";
|
|
654
827
|
this.emitter = options.emitter;
|
|
655
828
|
this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
|
|
829
|
+
this.allowNonDurable = options.allowNonDurable ?? false;
|
|
830
|
+
this.deferOpenDurabilityCheck = options.deferOpenDurabilityCheck ?? false;
|
|
831
|
+
this.releaseOnFreeze = options.releaseOnFreeze ?? true;
|
|
832
|
+
this.storage = options.storage ?? "opfs";
|
|
833
|
+
this.skipBackendRecord = options.skipBackendRecord ?? false;
|
|
834
|
+
this.onLeaderWorkerOpened = options.onLeaderWorkerOpened;
|
|
656
835
|
}
|
|
657
836
|
async open(schema) {
|
|
658
837
|
if (this.opened) return;
|
|
659
|
-
const ddlStatements = (0,
|
|
838
|
+
const ddlStatements = (0, import_core3.generateFullDDL)(schema);
|
|
660
839
|
if (this.injectedBridge) {
|
|
661
840
|
this.bridge = this.injectedBridge;
|
|
662
841
|
const response = await this.openCurrentBridge(ddlStatements);
|
|
663
842
|
this.reportStorageMode(response.data);
|
|
843
|
+
this.enforceDurability("open");
|
|
664
844
|
this.opened = true;
|
|
665
845
|
return;
|
|
666
846
|
}
|
|
667
847
|
if (this.workerUrl) {
|
|
668
848
|
this.warnSharedWorkerDeprecated();
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
849
|
+
this.schema = schema;
|
|
850
|
+
try {
|
|
851
|
+
const response = await this.attachAndOpen(ddlStatements);
|
|
852
|
+
this.reportStorageMode(response.data);
|
|
853
|
+
this.enforceDurability("open");
|
|
854
|
+
await this.afterLeaderOpen(response.data);
|
|
855
|
+
this.opened = true;
|
|
856
|
+
} catch (error) {
|
|
857
|
+
await this.teardown();
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
673
860
|
return;
|
|
674
861
|
}
|
|
675
862
|
throw new AdapterError(
|
|
676
863
|
'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
|
|
677
864
|
);
|
|
678
865
|
}
|
|
679
|
-
|
|
680
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Join the database as leader (own worker) or follower (RPC to the leader's
|
|
868
|
+
* worker), then open it. The open waits, without a timeout, while another
|
|
869
|
+
* holder still has the storage; the worker reports that as a blocking state.
|
|
870
|
+
*/
|
|
871
|
+
async attachAndOpen(ddlStatements) {
|
|
872
|
+
const workerUrl = this.workerUrl;
|
|
873
|
+
if (!workerUrl) {
|
|
681
874
|
throw new AdapterError("Durable SQLite WASM storage requires workerUrl.");
|
|
682
875
|
}
|
|
683
|
-
this.
|
|
684
|
-
this.tabSession = await acquireTabStorageSession(this.dbName, {
|
|
876
|
+
const session = await acquireTabStorageSession(this.dbName, {
|
|
685
877
|
onPromote: () => {
|
|
686
878
|
void this.promoteToLeader();
|
|
687
879
|
}
|
|
688
880
|
});
|
|
689
|
-
|
|
690
|
-
if (
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.
|
|
696
|
-
|
|
881
|
+
this.tabSession = session;
|
|
882
|
+
if (session.role === "leader") {
|
|
883
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
884
|
+
const worker = new WebWorkerBridge2(workerUrl, this.workerResponseTimeoutMs, {
|
|
885
|
+
onEvent: (event) => this.onWorkerEvent(event)
|
|
886
|
+
});
|
|
887
|
+
this.ownWorker = worker;
|
|
888
|
+
this.bridge = worker;
|
|
889
|
+
const response = await this.openLeaderWorker(worker, ddlStatements);
|
|
890
|
+
this.installLifecycleListeners();
|
|
891
|
+
return response;
|
|
697
892
|
}
|
|
698
893
|
const followerBridge = new FollowerBroadcastBridge(
|
|
699
|
-
|
|
894
|
+
session.channelName,
|
|
700
895
|
this.workerResponseTimeoutMs
|
|
701
896
|
);
|
|
702
897
|
this.bridge = followerBridge;
|
|
@@ -706,22 +901,152 @@ var SqliteWasmAdapter = class {
|
|
|
706
901
|
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.`
|
|
707
902
|
});
|
|
708
903
|
await followerBridge.waitForLeader();
|
|
904
|
+
this.installLifecycleListeners();
|
|
905
|
+
return this.checkedOpen(followerBridge, ddlStatements);
|
|
709
906
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
ddlStatements
|
|
715
|
-
|
|
716
|
-
|
|
907
|
+
/** Open the database in this tab's own worker and start serving followers. */
|
|
908
|
+
async openLeaderWorker(worker, ddlStatements) {
|
|
909
|
+
const hook = this.onLeaderWorkerOpened;
|
|
910
|
+
if (!hook) {
|
|
911
|
+
const pending = this.checkedOpen(worker, ddlStatements);
|
|
912
|
+
worker.post({ id: 0, type: "serve", channelName: this.tabSession?.channelName ?? "" });
|
|
913
|
+
return pending;
|
|
914
|
+
}
|
|
915
|
+
const response = await this.checkedOpen(worker, ddlStatements);
|
|
916
|
+
await runWorkerTransaction(worker, hook);
|
|
917
|
+
worker.post({ id: 0, type: "serve", channelName: this.tabSession?.channelName ?? "" });
|
|
918
|
+
return response;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Whether this runtime owns the database's worker: the storage leader in a browser,
|
|
922
|
+
* or any adapter with an injected bridge (tests, Node). A follower tab relays every
|
|
923
|
+
* request to the leader's worker (STORE-6).
|
|
924
|
+
*/
|
|
925
|
+
isLeader() {
|
|
926
|
+
if (this.injectedBridge) return true;
|
|
927
|
+
return this.tabSession?.role === "leader";
|
|
928
|
+
}
|
|
929
|
+
async checkedOpen(bridge, ddlStatements) {
|
|
930
|
+
const response = await bridge.send(
|
|
931
|
+
{
|
|
932
|
+
id: 0,
|
|
933
|
+
type: "open",
|
|
934
|
+
ddlStatements,
|
|
935
|
+
dbName: this.dbName,
|
|
936
|
+
...this.storage === "memory" ? { storage: "memory" } : {}
|
|
937
|
+
},
|
|
938
|
+
void 0,
|
|
939
|
+
{ timeoutMs: Number.POSITIVE_INFINITY }
|
|
940
|
+
);
|
|
717
941
|
if (response.type === "error") {
|
|
718
942
|
throw new AdapterError(`Failed to open database: ${response.message}`, {
|
|
719
943
|
code: response.code,
|
|
720
|
-
dbName: this.dbName
|
|
944
|
+
dbName: this.dbName,
|
|
945
|
+
...response.context ?? {}
|
|
721
946
|
});
|
|
722
947
|
}
|
|
723
948
|
return response;
|
|
724
949
|
}
|
|
950
|
+
async openCurrentBridge(ddlStatements) {
|
|
951
|
+
const bridge = this.bridge;
|
|
952
|
+
if (!bridge) throw new StoreNotOpenError();
|
|
953
|
+
return this.checkedOpen(bridge, ddlStatements);
|
|
954
|
+
}
|
|
955
|
+
onWorkerEvent(event) {
|
|
956
|
+
if (!this.emitter) return;
|
|
957
|
+
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";
|
|
958
|
+
if (event.kind === "storage-blocked") {
|
|
959
|
+
this.emitter.emit({
|
|
960
|
+
type: "store:storage-blocked",
|
|
961
|
+
dbName: this.dbName,
|
|
962
|
+
resource: event.resource,
|
|
963
|
+
state: "waiting",
|
|
964
|
+
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.`
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
this.emitter.emit({
|
|
969
|
+
type: "store:storage-blocked",
|
|
970
|
+
dbName: this.dbName,
|
|
971
|
+
resource: event.resource,
|
|
972
|
+
state: "resolved",
|
|
973
|
+
waitedMs: event.waitedMs,
|
|
974
|
+
message: `Database "${this.dbName}" obtained its storage after waiting ${event.waitedMs}ms.`
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Leader-only bookkeeping after a durable open: report a legacy-pool
|
|
979
|
+
* migration, move an IndexedDB-held copy into OPFS explicitly (never two
|
|
980
|
+
* disjoint copies), and record OPFS as this database's backend.
|
|
981
|
+
*/
|
|
982
|
+
async afterLeaderOpen(data) {
|
|
983
|
+
if (!this.ownWorker || this.skipBackendRecord || this.storageOpenState?.persistent !== true) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const info = typeof data === "object" && data !== null ? data : {};
|
|
987
|
+
if (info.migratedFromLegacy === true) {
|
|
988
|
+
this.emitter?.emit({
|
|
989
|
+
type: "store:storage-migrated",
|
|
990
|
+
dbName: this.dbName,
|
|
991
|
+
from: "legacy-opfs-pool",
|
|
992
|
+
to: "opfs",
|
|
993
|
+
message: `Database "${this.dbName}" moved from the shared pre-1.0 OPFS pool into its own pool.`
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (!isManifestAvailable()) return;
|
|
997
|
+
let recordedBackend = null;
|
|
998
|
+
try {
|
|
999
|
+
recordedBackend = (await readManifestRecord(this.dbName))?.backend ?? null;
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
console.warn(`[kora] Could not read the storage manifest for "${this.dbName}":`, error);
|
|
1002
|
+
}
|
|
1003
|
+
if (recordedBackend === "indexeddb") {
|
|
1004
|
+
const dump = await loadDumpFromIndexedDB(this.dbName);
|
|
1005
|
+
if (dump) {
|
|
1006
|
+
await this.applyDumpThroughWorker(this.ownWorker, dump);
|
|
1007
|
+
}
|
|
1008
|
+
this.emitter?.emit({
|
|
1009
|
+
type: "store:storage-migrated",
|
|
1010
|
+
dbName: this.dbName,
|
|
1011
|
+
from: "indexeddb",
|
|
1012
|
+
to: "opfs",
|
|
1013
|
+
message: `Database "${this.dbName}" moved from IndexedDB into OPFS.`
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
await recordDatabase({
|
|
1018
|
+
dbName: this.dbName,
|
|
1019
|
+
backend: "opfs",
|
|
1020
|
+
...this.storageOpenState.poolName ? { poolName: this.storageOpenState.poolName } : {}
|
|
1021
|
+
});
|
|
1022
|
+
if (recordedBackend === "indexeddb") {
|
|
1023
|
+
await deleteFromIndexedDB(this.dbName);
|
|
1024
|
+
}
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.warn(`[kora] Could not update the storage manifest for "${this.dbName}":`, error);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async applyDumpThroughWorker(worker, dump) {
|
|
1030
|
+
const check = async (request, what) => {
|
|
1031
|
+
const response = await worker.send(request);
|
|
1032
|
+
if (response.type === "error") {
|
|
1033
|
+
throw new AdapterError(`${what} failed: ${response.message}`, { dbName: this.dbName });
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
await check({ id: 0, type: "begin" }, "BEGIN IndexedDB import");
|
|
1037
|
+
try {
|
|
1038
|
+
for (const statement of restoreDumpStatements(dump)) {
|
|
1039
|
+
await check(
|
|
1040
|
+
{ id: 0, type: "execute", sql: statement.sql, params: statement.params },
|
|
1041
|
+
"IndexedDB import"
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
await check({ id: 0, type: "commit" }, "COMMIT IndexedDB import");
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
await worker.send({ id: 0, type: "rollback" }).catch(() => void 0);
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
725
1050
|
/**
|
|
726
1051
|
* Surface a `store:opfs-unavailable` diagnostic when the worker reported that
|
|
727
1052
|
* persistence silently degraded to an in-memory database, so a data-loss
|
|
@@ -733,7 +1058,7 @@ var SqliteWasmAdapter = class {
|
|
|
733
1058
|
if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
|
|
734
1059
|
return;
|
|
735
1060
|
}
|
|
736
|
-
if (this.storageOpenState.persistent === false) {
|
|
1061
|
+
if (this.storageOpenState.persistent === false && this.storage === "opfs") {
|
|
737
1062
|
const reason = this.storageOpenState.fallbackReason ?? "unsupported";
|
|
738
1063
|
this.emitter.emit({
|
|
739
1064
|
type: "store:opfs-unavailable",
|
|
@@ -746,6 +1071,42 @@ var SqliteWasmAdapter = class {
|
|
|
746
1071
|
getStorageOpenState() {
|
|
747
1072
|
return this.storageOpenState;
|
|
748
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Durable or loud, never silent (NEW-STORE-6). When the open or a promotion
|
|
1076
|
+
* ended on non-persistent storage and the app did not opt into that, emit the
|
|
1077
|
+
* blocking `store:durability-lost` event and refuse every later write. A
|
|
1078
|
+
* bridge that reports no mode (the Node mock) is not judged.
|
|
1079
|
+
*/
|
|
1080
|
+
enforceDurability(phase, failure) {
|
|
1081
|
+
if (this.allowNonDurable) return;
|
|
1082
|
+
if (phase === "open" && this.deferOpenDurabilityCheck && !failure) return;
|
|
1083
|
+
let reason;
|
|
1084
|
+
if (failure) {
|
|
1085
|
+
reason = failure.reason;
|
|
1086
|
+
} else if (this.storageOpenState?.persistent === false) {
|
|
1087
|
+
reason = this.storageOpenState.fallbackReason ?? "unsupported";
|
|
1088
|
+
} else {
|
|
1089
|
+
this.durabilityLoss = null;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
this.durabilityLoss = { phase, reason };
|
|
1093
|
+
this.emitter?.emit({
|
|
1094
|
+
type: "store:durability-lost",
|
|
1095
|
+
dbName: this.dbName,
|
|
1096
|
+
phase,
|
|
1097
|
+
reason,
|
|
1098
|
+
message: `Database "${this.dbName}" has no durable storage (${reason} during ${phase}); writes are refused until the app reloads with durable storage.${failure ? ` ${failure.message}` : ""}`
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
guardDurableWrite() {
|
|
1102
|
+
if (this.durabilityLoss) {
|
|
1103
|
+
throw new StorageDurabilityError(
|
|
1104
|
+
this.dbName,
|
|
1105
|
+
this.durabilityLoss.phase,
|
|
1106
|
+
this.durabilityLoss.reason
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
749
1110
|
warnSharedWorkerDeprecated() {
|
|
750
1111
|
if (!this.sharedWorkerUrl || warnedSharedWorkerDeprecated) {
|
|
751
1112
|
return;
|
|
@@ -755,71 +1116,255 @@ var SqliteWasmAdapter = class {
|
|
|
755
1116
|
"[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."
|
|
756
1117
|
);
|
|
757
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Close the database. A leader closes the database file and releases the OPFS
|
|
1121
|
+
* pool inside its worker, terminates the worker, and only then releases the
|
|
1122
|
+
* tab leader lock, so the next leader never races a still-running worker for
|
|
1123
|
+
* the file handles (NEW-STORE-10). A follower just leaves.
|
|
1124
|
+
*/
|
|
758
1125
|
async close() {
|
|
759
|
-
if (!this.bridge) return;
|
|
1126
|
+
if (!this.bridge && !this.tabSession) return;
|
|
1127
|
+
this.closing = true;
|
|
760
1128
|
try {
|
|
761
|
-
|
|
1129
|
+
if (this.injectedBridge && this.bridge) {
|
|
1130
|
+
await this.bridge.send({ id: 0, type: "close" });
|
|
1131
|
+
} else if (this.ownWorker && !this.ownWorker.isTerminated()) {
|
|
1132
|
+
const response = await this.ownWorker.send({ id: 0, type: "close" }, void 0, { timeoutMs: 5e3 }).catch((error) => ({
|
|
1133
|
+
id: 0,
|
|
1134
|
+
type: "error",
|
|
1135
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1136
|
+
code: "CLOSE_FAILED"
|
|
1137
|
+
}));
|
|
1138
|
+
if (response.type === "error") {
|
|
1139
|
+
console.warn(`[kora] Closing "${this.dbName}" in its worker failed:`, response.message);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
762
1142
|
} finally {
|
|
763
|
-
this.
|
|
764
|
-
this.
|
|
765
|
-
|
|
766
|
-
|
|
1143
|
+
await this.teardown();
|
|
1144
|
+
this.closing = false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Release everything this adapter holds, in ownership order: lifecycle
|
|
1149
|
+
* listeners, then the worker (which holds the OPFS pool and its Web Lock),
|
|
1150
|
+
* then the tab leader lock or queued promotion request.
|
|
1151
|
+
*/
|
|
1152
|
+
async teardown() {
|
|
1153
|
+
this.removeLifecycleListeners?.();
|
|
1154
|
+
this.removeLifecycleListeners = null;
|
|
1155
|
+
const session = this.tabSession;
|
|
1156
|
+
this.tabSession = null;
|
|
1157
|
+
session?.stopRelay?.();
|
|
1158
|
+
this.bridge?.terminate();
|
|
1159
|
+
this.bridge = null;
|
|
1160
|
+
this.ownWorker = null;
|
|
1161
|
+
if (session) {
|
|
1162
|
+
if (session.releaseLock) {
|
|
1163
|
+
await session.releaseLock();
|
|
767
1164
|
}
|
|
768
|
-
|
|
769
|
-
this.bridge.terminate();
|
|
770
|
-
this.bridge = null;
|
|
771
|
-
this.opened = false;
|
|
1165
|
+
session.cancelPromotionWatch?.();
|
|
772
1166
|
}
|
|
1167
|
+
this.opened = false;
|
|
1168
|
+
this.suspended = false;
|
|
1169
|
+
const gate = this.gate;
|
|
1170
|
+
this.gate = null;
|
|
1171
|
+
gate?.open();
|
|
773
1172
|
}
|
|
774
1173
|
/**
|
|
775
1174
|
* Promotes a follower to leader after the previous leader released the storage
|
|
776
|
-
* lock (its tab closed or
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
1175
|
+
* lock (its tab closed, crashed, or suspended on freeze). The new worker waits
|
|
1176
|
+
* on the pool's Web Lock and handles, so it never races the previous worker.
|
|
1177
|
+
* Requests issued during the hand-off wait for it; requests that were in
|
|
1178
|
+
* flight to the old leader fail with a retriable `BridgeTerminatedError`.
|
|
780
1179
|
*/
|
|
781
1180
|
async promoteToLeader() {
|
|
782
|
-
if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
1181
|
+
if (this.promoting || this.closing || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
783
1182
|
return;
|
|
784
1183
|
}
|
|
785
1184
|
this.promoting = true;
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1185
|
+
const gate = createGate();
|
|
1186
|
+
this.gate = gate;
|
|
1187
|
+
try {
|
|
1188
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
1189
|
+
if (this.closing || !this.opened) return;
|
|
1190
|
+
const worker = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs, {
|
|
1191
|
+
onEvent: (event) => this.onWorkerEvent(event)
|
|
1192
|
+
});
|
|
1193
|
+
const previousBridge = this.bridge;
|
|
1194
|
+
this.bridge = worker;
|
|
1195
|
+
this.ownWorker = worker;
|
|
1196
|
+
if (this.tabSession) {
|
|
1197
|
+
this.tabSession.role = "leader";
|
|
1198
|
+
}
|
|
1199
|
+
previousBridge?.terminate();
|
|
1200
|
+
const ddlStatements = (0, import_core3.generateFullDDL)(this.schema);
|
|
1201
|
+
let response;
|
|
1202
|
+
try {
|
|
1203
|
+
response = await this.openLeaderWorker(worker, ddlStatements);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.storageOpenState = { persistent: false, mode: "memory" };
|
|
1206
|
+
this.enforceDurability("promotion", {
|
|
1207
|
+
reason: "open-failed",
|
|
1208
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1209
|
+
});
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
this.storageOpenState = parseStorageOpenState(response.data) ?? this.storageOpenState;
|
|
1213
|
+
this.enforceDurability("promotion");
|
|
1214
|
+
try {
|
|
1215
|
+
await this.afterLeaderOpen(response.data);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
this.enforceDurability("promotion", {
|
|
1218
|
+
reason: "open-failed",
|
|
1219
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
} finally {
|
|
1223
|
+
this.promoting = false;
|
|
1224
|
+
if (this.gate === gate) this.gate = null;
|
|
1225
|
+
gate.open();
|
|
795
1226
|
}
|
|
796
|
-
previousBridge?.terminate();
|
|
797
|
-
const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
|
|
798
|
-
await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
|
|
799
1227
|
}
|
|
800
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* Page Lifecycle: a frozen or bfcached page cannot run, so it must not keep
|
|
1230
|
+
* other tabs waiting on its storage. Leaders terminate their worker (freeing
|
|
1231
|
+
* the OPFS pool) and then release the leader lock, so a visible tab is
|
|
1232
|
+
* promoted; followers withdraw their queued lock request so a frozen tab is
|
|
1233
|
+
* never granted leadership it cannot use. Never uses Web Locks `steal`.
|
|
1234
|
+
*/
|
|
1235
|
+
installLifecycleListeners() {
|
|
1236
|
+
if (!this.releaseOnFreeze || this.removeLifecycleListeners) return;
|
|
1237
|
+
if (typeof addEventListener !== "function" || typeof document === "undefined") return;
|
|
1238
|
+
const onFreeze = () => this.suspend();
|
|
1239
|
+
const onPageHide = () => this.suspend();
|
|
1240
|
+
const onResume = () => {
|
|
1241
|
+
void this.resume();
|
|
1242
|
+
};
|
|
1243
|
+
const onPageShow = (event) => {
|
|
1244
|
+
if (event.persisted) void this.resume();
|
|
1245
|
+
};
|
|
1246
|
+
document.addEventListener("freeze", onFreeze);
|
|
1247
|
+
document.addEventListener("resume", onResume);
|
|
1248
|
+
addEventListener("pagehide", onPageHide);
|
|
1249
|
+
addEventListener("pageshow", onPageShow);
|
|
1250
|
+
this.removeLifecycleListeners = () => {
|
|
1251
|
+
document.removeEventListener("freeze", onFreeze);
|
|
1252
|
+
document.removeEventListener("resume", onResume);
|
|
1253
|
+
removeEventListener("pagehide", onPageHide);
|
|
1254
|
+
removeEventListener("pageshow", onPageShow);
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
/** Synchronous by necessity: a `freeze` handler gets no further turns. */
|
|
1258
|
+
suspend() {
|
|
1259
|
+
if (!this.opened || this.suspended || this.closing || this.promoting) return;
|
|
1260
|
+
this.suspended = true;
|
|
1261
|
+
this.gate = createGate();
|
|
1262
|
+
const session = this.tabSession;
|
|
1263
|
+
this.tabSession = null;
|
|
1264
|
+
this.bridge?.terminate();
|
|
1265
|
+
this.bridge = null;
|
|
1266
|
+
this.ownWorker = null;
|
|
1267
|
+
if (session) {
|
|
1268
|
+
void session.releaseLock?.();
|
|
1269
|
+
session.cancelPromotionWatch?.();
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
async resume() {
|
|
1273
|
+
if (!this.suspended || this.closing || this.schema === null) return;
|
|
1274
|
+
const gate = this.gate;
|
|
1275
|
+
try {
|
|
1276
|
+
const response = await this.attachAndOpen((0, import_core3.generateFullDDL)(this.schema));
|
|
1277
|
+
this.reportStorageMode(response.data);
|
|
1278
|
+
this.enforceDurability("promotion");
|
|
1279
|
+
await this.afterLeaderOpen(response.data);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
this.enforceDurability("promotion", {
|
|
1282
|
+
reason: "open-failed",
|
|
1283
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1284
|
+
});
|
|
1285
|
+
} finally {
|
|
1286
|
+
this.suspended = false;
|
|
1287
|
+
if (this.gate === gate) this.gate = null;
|
|
1288
|
+
gate?.open();
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Non-transactional write. Takes the same mutex as {@link transaction}, so it
|
|
1293
|
+
* never lands inside this tab's open transaction and is never rolled back
|
|
1294
|
+
* with it (STORE-8). Inside a transaction callback, use the `tx` handle.
|
|
1295
|
+
*/
|
|
1296
|
+
async execute(sql, params, options) {
|
|
801
1297
|
this.guardOpen();
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1298
|
+
this.guardDurableWrite();
|
|
1299
|
+
const release = await this.mutex.acquire();
|
|
1300
|
+
try {
|
|
1301
|
+
const response = await this.sendRequest(
|
|
1302
|
+
{
|
|
1303
|
+
id: 0,
|
|
1304
|
+
type: "execute",
|
|
1305
|
+
sql,
|
|
1306
|
+
params,
|
|
1307
|
+
...options?.requestId ? { requestId: options.requestId } : {}
|
|
1308
|
+
},
|
|
1309
|
+
options?.signal ? { signal: options.signal } : void 0
|
|
1310
|
+
);
|
|
1311
|
+
if (response.type === "error") {
|
|
1312
|
+
throw new AdapterError(`Execute failed: ${response.message}`, { sql, params });
|
|
1313
|
+
}
|
|
1314
|
+
} finally {
|
|
1315
|
+
release();
|
|
805
1316
|
}
|
|
806
1317
|
}
|
|
807
|
-
|
|
1318
|
+
/**
|
|
1319
|
+
* Non-transactional read. Waits for this tab's open transaction to finish, so
|
|
1320
|
+
* it never observes uncommitted (possibly rolled-back) rows (STORE-8).
|
|
1321
|
+
*/
|
|
1322
|
+
async query(sql, params, options) {
|
|
808
1323
|
this.guardOpen();
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
1324
|
+
const release = await this.mutex.acquire();
|
|
1325
|
+
try {
|
|
1326
|
+
const response = await this.sendRequest(
|
|
1327
|
+
{
|
|
1328
|
+
id: 0,
|
|
1329
|
+
type: "query",
|
|
1330
|
+
sql,
|
|
1331
|
+
params,
|
|
1332
|
+
...options?.requestId ? { requestId: options.requestId } : {}
|
|
1333
|
+
},
|
|
1334
|
+
options?.signal ? { signal: options.signal } : void 0
|
|
1335
|
+
);
|
|
1336
|
+
if (response.type === "error") {
|
|
1337
|
+
throw new AdapterError(`Query failed: ${response.message}`, { sql, params });
|
|
1338
|
+
}
|
|
1339
|
+
return response.data ?? [];
|
|
1340
|
+
} finally {
|
|
1341
|
+
release();
|
|
812
1342
|
}
|
|
813
|
-
return response.data ?? [];
|
|
814
1343
|
}
|
|
815
1344
|
async transaction(fn) {
|
|
816
1345
|
this.guardOpen();
|
|
1346
|
+
this.guardDurableWrite();
|
|
817
1347
|
const release = await this.mutex.acquire();
|
|
1348
|
+
let txBridge = null;
|
|
1349
|
+
const sendInSpan = async (request) => {
|
|
1350
|
+
if (txBridge === null || this.bridge !== txBridge) {
|
|
1351
|
+
throw new BridgeTerminatedError(request.type, "storage leader changed during transaction");
|
|
1352
|
+
}
|
|
1353
|
+
return txBridge.send(request);
|
|
1354
|
+
};
|
|
818
1355
|
try {
|
|
819
|
-
|
|
1356
|
+
while (this.gate) {
|
|
1357
|
+
await this.gate.promise;
|
|
1358
|
+
}
|
|
1359
|
+
txBridge = this.bridge;
|
|
1360
|
+
if (!txBridge) throw new StoreNotOpenError();
|
|
1361
|
+
const begin = await sendInSpan({ id: 0, type: "begin" });
|
|
1362
|
+
if (begin.type === "error") {
|
|
1363
|
+
throw new AdapterError(`BEGIN transaction failed: ${begin.message}`);
|
|
1364
|
+
}
|
|
820
1365
|
const tx = {
|
|
821
1366
|
execute: async (sql, params) => {
|
|
822
|
-
const response = await
|
|
1367
|
+
const response = await sendInSpan({ id: 0, type: "execute", sql, params });
|
|
823
1368
|
if (response.type === "error") {
|
|
824
1369
|
throw new AdapterError(`Transaction execute failed: ${response.message}`, {
|
|
825
1370
|
sql,
|
|
@@ -828,7 +1373,7 @@ var SqliteWasmAdapter = class {
|
|
|
828
1373
|
}
|
|
829
1374
|
},
|
|
830
1375
|
query: async (sql, params) => {
|
|
831
|
-
const response = await
|
|
1376
|
+
const response = await sendInSpan({ id: 0, type: "query", sql, params });
|
|
832
1377
|
if (response.type === "error") {
|
|
833
1378
|
throw new AdapterError(`Transaction query failed: ${response.message}`, { sql, params });
|
|
834
1379
|
}
|
|
@@ -836,10 +1381,15 @@ var SqliteWasmAdapter = class {
|
|
|
836
1381
|
}
|
|
837
1382
|
};
|
|
838
1383
|
await fn(tx);
|
|
839
|
-
await
|
|
1384
|
+
const commit = await sendInSpan({ id: 0, type: "commit" });
|
|
1385
|
+
if (commit.type === "error") {
|
|
1386
|
+
throw new AdapterError(`COMMIT transaction failed: ${commit.message}`);
|
|
1387
|
+
}
|
|
840
1388
|
} catch (error) {
|
|
841
1389
|
try {
|
|
842
|
-
|
|
1390
|
+
if (txBridge && this.bridge === txBridge) {
|
|
1391
|
+
await txBridge.send({ id: 0, type: "rollback" });
|
|
1392
|
+
}
|
|
843
1393
|
} catch {
|
|
844
1394
|
}
|
|
845
1395
|
throw error;
|
|
@@ -849,6 +1399,7 @@ var SqliteWasmAdapter = class {
|
|
|
849
1399
|
}
|
|
850
1400
|
async migrate(from, to, migration) {
|
|
851
1401
|
this.guardOpen();
|
|
1402
|
+
this.guardDurableWrite();
|
|
852
1403
|
const release = await this.mutex.acquire();
|
|
853
1404
|
try {
|
|
854
1405
|
await this.sendChecked({ id: 0, type: "begin" }, "BEGIN migration");
|
|
@@ -882,36 +1433,50 @@ var SqliteWasmAdapter = class {
|
|
|
882
1433
|
*/
|
|
883
1434
|
async exportDatabase() {
|
|
884
1435
|
this.guardOpen();
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1436
|
+
const release = await this.mutex.acquire();
|
|
1437
|
+
try {
|
|
1438
|
+
const response = await this.sendRequest({ id: 0, type: "export" });
|
|
1439
|
+
if (response.type === "error") {
|
|
1440
|
+
throw new AdapterError(`Export failed: ${response.message}`, {
|
|
1441
|
+
code: response.code,
|
|
1442
|
+
context: response.context
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
return response.data;
|
|
1446
|
+
} finally {
|
|
1447
|
+
release();
|
|
891
1448
|
}
|
|
892
|
-
return response.data;
|
|
893
1449
|
}
|
|
894
1450
|
/**
|
|
895
1451
|
* Import a serialized database snapshot.
|
|
896
1452
|
*/
|
|
897
1453
|
async importDatabase(data) {
|
|
898
1454
|
this.guardOpen();
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1455
|
+
this.guardDurableWrite();
|
|
1456
|
+
const release = await this.mutex.acquire();
|
|
1457
|
+
try {
|
|
1458
|
+
const response = await this.sendRequest({ id: 0, type: "import", data });
|
|
1459
|
+
if (response.type === "error") {
|
|
1460
|
+
throw new AdapterError(`Import failed: ${response.message}`);
|
|
1461
|
+
}
|
|
1462
|
+
} finally {
|
|
1463
|
+
release();
|
|
902
1464
|
}
|
|
903
1465
|
}
|
|
904
1466
|
guardOpen() {
|
|
905
|
-
if (!this.opened || !this.bridge) {
|
|
1467
|
+
if (!this.opened || !this.bridge && !this.gate) {
|
|
906
1468
|
throw new StoreNotOpenError();
|
|
907
1469
|
}
|
|
908
1470
|
}
|
|
909
|
-
async sendRequest(request) {
|
|
1471
|
+
async sendRequest(request, options) {
|
|
1472
|
+
while (this.gate) {
|
|
1473
|
+
await this.gate.promise;
|
|
1474
|
+
}
|
|
910
1475
|
const bridge = this.bridge;
|
|
911
1476
|
if (!bridge) {
|
|
912
1477
|
throw new StoreNotOpenError();
|
|
913
1478
|
}
|
|
914
|
-
return bridge.send(request);
|
|
1479
|
+
return bridge.send(request, void 0, options);
|
|
915
1480
|
}
|
|
916
1481
|
async sendChecked(request, description) {
|
|
917
1482
|
const response = await this.sendRequest(request);
|
|
@@ -920,6 +1485,34 @@ var SqliteWasmAdapter = class {
|
|
|
920
1485
|
}
|
|
921
1486
|
}
|
|
922
1487
|
};
|
|
1488
|
+
async function runWorkerTransaction(worker, fn) {
|
|
1489
|
+
const send = async (request, what) => {
|
|
1490
|
+
const response = await worker.send(request);
|
|
1491
|
+
if (response.type === "error") {
|
|
1492
|
+
throw new AdapterError(`${what} failed: ${response.message}`, {
|
|
1493
|
+
code: response.code,
|
|
1494
|
+
...request.type === "execute" || request.type === "query" ? { sql: request.sql } : {}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return response;
|
|
1498
|
+
};
|
|
1499
|
+
await send({ id: 0, type: "begin" }, "BEGIN");
|
|
1500
|
+
try {
|
|
1501
|
+
await fn({
|
|
1502
|
+
execute: async (sql, params) => {
|
|
1503
|
+
await send({ id: 0, type: "execute", sql, params }, "Leader open execute");
|
|
1504
|
+
},
|
|
1505
|
+
query: async (sql, params) => {
|
|
1506
|
+
const response = await send({ id: 0, type: "query", sql, params }, "Leader open query");
|
|
1507
|
+
return response.data ?? [];
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
await send({ id: 0, type: "commit" }, "COMMIT");
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
await worker.send({ id: 0, type: "rollback" }).catch(() => void 0);
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
923
1516
|
function parseStorageOpenState(data) {
|
|
924
1517
|
if (typeof data !== "object" || data === null) {
|
|
925
1518
|
return null;
|
|
@@ -931,16 +1524,111 @@ function parseStorageOpenState(data) {
|
|
|
931
1524
|
return {
|
|
932
1525
|
persistent: mode.persistent,
|
|
933
1526
|
mode: mode.persistent ? "opfs" : "memory",
|
|
934
|
-
...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {}
|
|
1527
|
+
...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {},
|
|
1528
|
+
...typeof mode.poolName === "string" ? { poolName: mode.poolName } : {},
|
|
1529
|
+
...typeof mode.journalMode === "string" ? { journalMode: mode.journalMode } : {}
|
|
935
1530
|
};
|
|
936
1531
|
}
|
|
937
1532
|
|
|
938
1533
|
// src/adapters/sqlite-wasm.ts
|
|
939
1534
|
init_sqlite_wasm_channel();
|
|
1535
|
+
|
|
1536
|
+
// src/adapters/local-databases.ts
|
|
1537
|
+
init_errors();
|
|
1538
|
+
async function listLocalDatabases() {
|
|
1539
|
+
return listManifestRecords();
|
|
1540
|
+
}
|
|
1541
|
+
async function withExclusiveDatabase(dbName, run) {
|
|
1542
|
+
const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
1543
|
+
if (!locks || typeof locks.request !== "function") {
|
|
1544
|
+
return run();
|
|
1545
|
+
}
|
|
1546
|
+
let ran = false;
|
|
1547
|
+
let result;
|
|
1548
|
+
await locks.request(
|
|
1549
|
+
leaderLockName(dbName),
|
|
1550
|
+
{ mode: "exclusive", ifAvailable: true },
|
|
1551
|
+
async (lock) => {
|
|
1552
|
+
if (lock === null) return;
|
|
1553
|
+
ran = true;
|
|
1554
|
+
result = await run();
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
if (!ran) {
|
|
1558
|
+
throw new StorageInUseError(dbName);
|
|
1559
|
+
}
|
|
1560
|
+
return result;
|
|
1561
|
+
}
|
|
1562
|
+
async function deleteLocalDatabase(dbName, options) {
|
|
1563
|
+
const record = await readManifestRecord(dbName);
|
|
1564
|
+
const backend = record?.backend ?? "opfs";
|
|
1565
|
+
return withExclusiveDatabase(dbName, async () => {
|
|
1566
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
1567
|
+
const worker = new WebWorkerBridge2(options.workerUrl, options.workerResponseTimeoutMs ?? 3e4);
|
|
1568
|
+
const send = async (request) => worker.send(request);
|
|
1569
|
+
const reader = {
|
|
1570
|
+
dbName,
|
|
1571
|
+
query: async (sql, params) => {
|
|
1572
|
+
const response = await send({ id: 0, type: "query", sql, params });
|
|
1573
|
+
if (response.type === "error") {
|
|
1574
|
+
throw new AdapterError(`Query failed: ${response.message}`, { dbName, sql });
|
|
1575
|
+
}
|
|
1576
|
+
return response.data ?? [];
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
try {
|
|
1580
|
+
let exists;
|
|
1581
|
+
if (backend === "opfs") {
|
|
1582
|
+
const opened = await worker.send(
|
|
1583
|
+
{ id: 0, type: "open", ddlStatements: [], dbName, mustExist: true },
|
|
1584
|
+
void 0,
|
|
1585
|
+
{ timeoutMs: Number.POSITIVE_INFINITY }
|
|
1586
|
+
);
|
|
1587
|
+
if (opened.type === "error" && opened.code !== "NOT_FOUND") {
|
|
1588
|
+
throw new AdapterError(`Could not open "${dbName}" for deletion: ${opened.message}`, {
|
|
1589
|
+
dbName,
|
|
1590
|
+
code: opened.code
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
exists = opened.type === "success";
|
|
1594
|
+
} else {
|
|
1595
|
+
const dump = await loadDumpFromIndexedDB(dbName);
|
|
1596
|
+
exists = dump !== null;
|
|
1597
|
+
if (dump) {
|
|
1598
|
+
await send({ id: 0, type: "open", ddlStatements: [], dbName, storage: "memory" });
|
|
1599
|
+
for (const statement of restoreDumpStatements(dump, true)) {
|
|
1600
|
+
await send({ id: 0, type: "execute", sql: statement.sql, params: statement.params });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (exists && !options.force && options.hasUnsyncedOperations) {
|
|
1605
|
+
if (await options.hasUnsyncedOperations(reader)) {
|
|
1606
|
+
throw new UnsyncedDataError(dbName);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (backend === "opfs" && exists) {
|
|
1610
|
+
const destroyed = await send({ id: 0, type: "destroy" });
|
|
1611
|
+
if (destroyed.type === "error") {
|
|
1612
|
+
throw new AdapterError(`Could not delete "${dbName}": ${destroyed.message}`, { dbName });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (backend === "indexeddb") {
|
|
1616
|
+
await deleteFromIndexedDB(dbName);
|
|
1617
|
+
}
|
|
1618
|
+
await deleteManifestRecord(dbName);
|
|
1619
|
+
return exists || record !== null;
|
|
1620
|
+
} finally {
|
|
1621
|
+
worker.terminate();
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
940
1625
|
// Annotate the CommonJS export names for ESM import in node:
|
|
941
1626
|
0 && (module.exports = {
|
|
942
1627
|
Mutex,
|
|
943
1628
|
SqliteWasmAdapter,
|
|
944
|
-
WebWorkerBridge
|
|
1629
|
+
WebWorkerBridge,
|
|
1630
|
+
deleteLocalDatabase,
|
|
1631
|
+
listLocalDatabases,
|
|
1632
|
+
opfsPoolNameFor
|
|
945
1633
|
});
|
|
946
1634
|
//# sourceMappingURL=sqlite-wasm.cjs.map
|