@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
package/dist/chunk-RJIEKEN4.js
DELETED
|
@@ -1,751 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Mutex
|
|
3
|
-
} from "./chunk-TVJEC5S2.js";
|
|
4
|
-
import {
|
|
5
|
-
AdapterError,
|
|
6
|
-
NoLeaderError,
|
|
7
|
-
StoreNotOpenError,
|
|
8
|
-
WorkerTimeoutError
|
|
9
|
-
} from "./chunk-TUR7RGEP.js";
|
|
10
|
-
|
|
11
|
-
// src/adapters/sqlite-wasm-adapter.ts
|
|
12
|
-
import { generateFullDDL } from "@korajs/core";
|
|
13
|
-
|
|
14
|
-
// src/multi-tab/tab-storage.ts
|
|
15
|
-
var RPC_REQUEST = "kora-worker-request";
|
|
16
|
-
var RPC_RESPONSE = "kora-worker-response";
|
|
17
|
-
var CLIENT_LEAVE = "kora-client-leave";
|
|
18
|
-
var LEADER_PING = "kora-leader-ping";
|
|
19
|
-
var LEADER_PONG = "kora-leader-pong";
|
|
20
|
-
var DEFAULT_LIVENESS_PROBE_MS = 2e3;
|
|
21
|
-
var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
|
|
22
|
-
function isMultiTabStorageSupported() {
|
|
23
|
-
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
24
|
-
}
|
|
25
|
-
async function acquireTabStorageSession(dbName, options) {
|
|
26
|
-
const channelName = `kora-storage-${dbName}`;
|
|
27
|
-
const lockName = `kora-leader-${dbName}`;
|
|
28
|
-
if (!isMultiTabStorageSupported()) {
|
|
29
|
-
return { role: "leader", channelName };
|
|
30
|
-
}
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
let releaseHeld;
|
|
33
|
-
void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
|
|
34
|
-
if (lock === null) {
|
|
35
|
-
resolve(startFollowerSession(channelName, lockName, options));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
resolve({
|
|
39
|
-
role: "leader",
|
|
40
|
-
channelName,
|
|
41
|
-
releaseLock: async () => {
|
|
42
|
-
releaseHeld?.();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
return new Promise((release) => {
|
|
46
|
-
releaseHeld = release;
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function startFollowerSession(channelName, lockName, options) {
|
|
52
|
-
let promoted = false;
|
|
53
|
-
let releasePromotedLock;
|
|
54
|
-
const abort = new AbortController();
|
|
55
|
-
void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
|
|
56
|
-
promoted = true;
|
|
57
|
-
options?.onPromote?.();
|
|
58
|
-
return new Promise((release) => {
|
|
59
|
-
releasePromotedLock = release;
|
|
60
|
-
});
|
|
61
|
-
}).catch(() => {
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
role: "follower",
|
|
65
|
-
channelName,
|
|
66
|
-
cancelPromotionWatch: () => {
|
|
67
|
-
if (promoted) {
|
|
68
|
-
releasePromotedLock?.();
|
|
69
|
-
} else {
|
|
70
|
-
abort.abort();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function startLeaderRpcRelay(channelName, bridge) {
|
|
76
|
-
const channel = new BroadcastChannel(channelName);
|
|
77
|
-
const onMessage = (event) => {
|
|
78
|
-
const data = event.data;
|
|
79
|
-
if (data?.type === LEADER_PING) {
|
|
80
|
-
channel.postMessage({ type: LEADER_PONG });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (data?.type === CLIENT_LEAVE) {
|
|
84
|
-
if (hasReclaimClient(bridge)) {
|
|
85
|
-
bridge.reclaimClient(data.clientId, "client-left");
|
|
86
|
-
}
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (data?.type !== RPC_REQUEST) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
void bridge.send(data.request, data.clientId).then((response) => {
|
|
93
|
-
const msg = {
|
|
94
|
-
type: RPC_RESPONSE,
|
|
95
|
-
requestId: data.requestId,
|
|
96
|
-
response
|
|
97
|
-
};
|
|
98
|
-
channel.postMessage(msg);
|
|
99
|
-
}).catch((error) => {
|
|
100
|
-
const message = error instanceof Error ? error.message : "Worker RPC failed";
|
|
101
|
-
const msg = {
|
|
102
|
-
type: RPC_RESPONSE,
|
|
103
|
-
requestId: data.requestId,
|
|
104
|
-
response: {
|
|
105
|
-
id: data.request.id,
|
|
106
|
-
type: "error",
|
|
107
|
-
message,
|
|
108
|
-
code: "LEADER_RPC_ERROR"
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
channel.postMessage(msg);
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
channel.addEventListener("message", onMessage);
|
|
115
|
-
return () => {
|
|
116
|
-
channel.removeEventListener("message", onMessage);
|
|
117
|
-
channel.close();
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
var FollowerBroadcastBridge = class {
|
|
121
|
-
channel;
|
|
122
|
-
clientId = createClientId();
|
|
123
|
-
onPageHide = () => {
|
|
124
|
-
this.leaveLeader();
|
|
125
|
-
};
|
|
126
|
-
pending = /* @__PURE__ */ new Map();
|
|
127
|
-
timeoutMs;
|
|
128
|
-
livenessProbeMs;
|
|
129
|
-
terminated = false;
|
|
130
|
-
constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
|
|
131
|
-
this.timeoutMs = timeoutMs;
|
|
132
|
-
this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
|
|
133
|
-
this.channel = new BroadcastChannel(channelName);
|
|
134
|
-
if (typeof addEventListener === "function") {
|
|
135
|
-
addEventListener("pagehide", this.onPageHide);
|
|
136
|
-
}
|
|
137
|
-
this.channel.addEventListener("message", (event) => {
|
|
138
|
-
const data = event.data;
|
|
139
|
-
if (data?.type !== RPC_RESPONSE) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const entry = this.pending.get(data.requestId);
|
|
143
|
-
if (entry) {
|
|
144
|
-
this.pending.delete(data.requestId);
|
|
145
|
-
entry.resolve(data.response);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
151
|
-
* or `false` if none answers within the budget. The adapter calls this before
|
|
152
|
-
* its first RPC so a follower created before a leader relay is live retries the
|
|
153
|
-
* handshake instead of firing into the void and waiting out the full timeout.
|
|
154
|
-
*/
|
|
155
|
-
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
156
|
-
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
157
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
158
|
-
if (this.terminated) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
if (await this.pingLeader(perAttempt)) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
/** Sends one liveness ping and resolves whether a leader answered in time. */
|
|
168
|
-
pingLeader(timeoutMs) {
|
|
169
|
-
if (this.terminated) {
|
|
170
|
-
return Promise.resolve(false);
|
|
171
|
-
}
|
|
172
|
-
return new Promise((resolve) => {
|
|
173
|
-
const onPong = (event) => {
|
|
174
|
-
if (event.data?.type === LEADER_PONG) {
|
|
175
|
-
cleanup();
|
|
176
|
-
resolve(true);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const cleanup = () => {
|
|
180
|
-
clearTimeout(timer);
|
|
181
|
-
this.channel.removeEventListener("message", onPong);
|
|
182
|
-
};
|
|
183
|
-
const timer = setTimeout(() => {
|
|
184
|
-
cleanup();
|
|
185
|
-
resolve(false);
|
|
186
|
-
}, timeoutMs);
|
|
187
|
-
this.channel.addEventListener("message", onPong);
|
|
188
|
-
this.channel.postMessage({ type: LEADER_PING });
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
async send(request) {
|
|
192
|
-
if (this.terminated) {
|
|
193
|
-
return {
|
|
194
|
-
id: request.id,
|
|
195
|
-
type: "error",
|
|
196
|
-
message: "Follower bridge terminated",
|
|
197
|
-
code: "BRIDGE_TERMINATED"
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
const requestId = createClientId();
|
|
201
|
-
const msg = {
|
|
202
|
-
type: RPC_REQUEST,
|
|
203
|
-
requestId,
|
|
204
|
-
clientId: this.clientId,
|
|
205
|
-
request
|
|
206
|
-
};
|
|
207
|
-
return new Promise((resolve, reject) => {
|
|
208
|
-
const settle = (fn) => {
|
|
209
|
-
clearTimeout(timer);
|
|
210
|
-
clearTimeout(probe);
|
|
211
|
-
this.pending.delete(requestId);
|
|
212
|
-
fn();
|
|
213
|
-
};
|
|
214
|
-
const timer = setTimeout(() => {
|
|
215
|
-
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
|
|
216
|
-
}, this.timeoutMs);
|
|
217
|
-
const probe = setTimeout(() => {
|
|
218
|
-
void this.pingLeader(this.livenessProbeMs).then((alive) => {
|
|
219
|
-
if (!alive && this.pending.has(requestId)) {
|
|
220
|
-
settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}, this.livenessProbeMs);
|
|
224
|
-
this.pending.set(requestId, {
|
|
225
|
-
resolve: (response) => settle(() => resolve(response)),
|
|
226
|
-
reject: (error) => settle(() => reject(error))
|
|
227
|
-
});
|
|
228
|
-
this.channel.postMessage(msg);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
terminate() {
|
|
232
|
-
if (this.terminated) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
this.terminated = true;
|
|
236
|
-
this.leaveLeader();
|
|
237
|
-
if (typeof removeEventListener === "function") {
|
|
238
|
-
removeEventListener("pagehide", this.onPageHide);
|
|
239
|
-
}
|
|
240
|
-
this.channel.close();
|
|
241
|
-
for (const [, entry] of this.pending) {
|
|
242
|
-
entry.reject(new Error("Follower bridge terminated"));
|
|
243
|
-
}
|
|
244
|
-
this.pending.clear();
|
|
245
|
-
}
|
|
246
|
-
leaveLeader() {
|
|
247
|
-
try {
|
|
248
|
-
const message = {
|
|
249
|
-
type: CLIENT_LEAVE,
|
|
250
|
-
clientId: this.clientId
|
|
251
|
-
};
|
|
252
|
-
this.channel.postMessage(message);
|
|
253
|
-
} catch {
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
var TransactionSerializingWorkerBridge = class {
|
|
258
|
-
inner;
|
|
259
|
-
leaderClientId = createClientId();
|
|
260
|
-
transactionIdleTimeoutMs;
|
|
261
|
-
queue = [];
|
|
262
|
-
activeTransactionClient = null;
|
|
263
|
-
abortedClients = /* @__PURE__ */ new Set();
|
|
264
|
-
transactionIdleTimer = null;
|
|
265
|
-
reclaiming = false;
|
|
266
|
-
processing = false;
|
|
267
|
-
terminated = false;
|
|
268
|
-
constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
|
|
269
|
-
this.inner = inner;
|
|
270
|
-
this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
|
|
271
|
-
}
|
|
272
|
-
send(request, clientId = this.leaderClientId) {
|
|
273
|
-
if (this.terminated) {
|
|
274
|
-
return Promise.resolve({
|
|
275
|
-
id: request.id,
|
|
276
|
-
type: "error",
|
|
277
|
-
message: "Worker has been terminated",
|
|
278
|
-
code: "WORKER_TERMINATED"
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
if (this.abortedClients.has(clientId)) {
|
|
282
|
-
if (request.type === "begin") {
|
|
283
|
-
this.abortedClients.delete(clientId);
|
|
284
|
-
} else {
|
|
285
|
-
return Promise.resolve({
|
|
286
|
-
id: request.id,
|
|
287
|
-
type: "error",
|
|
288
|
-
message: "Previous transaction was aborted because the client stopped sending requests.",
|
|
289
|
-
code: "TRANSACTION_ABORTED"
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return new Promise((resolve, reject) => {
|
|
294
|
-
this.queue.push({ clientId, request, resolve, reject });
|
|
295
|
-
void this.processQueue();
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
terminate() {
|
|
299
|
-
if (this.terminated) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
this.terminated = true;
|
|
303
|
-
this.clearTransactionIdleTimer();
|
|
304
|
-
this.inner.terminate();
|
|
305
|
-
const pending = this.queue.splice(0);
|
|
306
|
-
for (const entry of pending) {
|
|
307
|
-
entry.reject(new Error("Worker terminated"));
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
reclaimClient(clientId, reason) {
|
|
311
|
-
if (this.terminated) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
void this.reclaimClientNow(clientId, reason);
|
|
315
|
-
}
|
|
316
|
-
async processQueue() {
|
|
317
|
-
if (this.processing || this.reclaiming) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
this.processing = true;
|
|
321
|
-
try {
|
|
322
|
-
while (!this.terminated) {
|
|
323
|
-
const index = this.nextRunnableIndex();
|
|
324
|
-
if (index === -1) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const [entry] = this.queue.splice(index, 1);
|
|
328
|
-
if (!entry) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
try {
|
|
332
|
-
const response = await this.inner.send(entry.request);
|
|
333
|
-
this.recordTransactionState(entry, response);
|
|
334
|
-
entry.resolve(response);
|
|
335
|
-
} catch (error) {
|
|
336
|
-
this.resetFailedTransaction(entry);
|
|
337
|
-
entry.reject(error instanceof Error ? error : new Error(String(error)));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} finally {
|
|
341
|
-
this.processing = false;
|
|
342
|
-
if (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {
|
|
343
|
-
void this.processQueue();
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
nextRunnableIndex() {
|
|
348
|
-
if (this.queue.length === 0) {
|
|
349
|
-
return -1;
|
|
350
|
-
}
|
|
351
|
-
if (this.activeTransactionClient === null) {
|
|
352
|
-
return 0;
|
|
353
|
-
}
|
|
354
|
-
return this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient);
|
|
355
|
-
}
|
|
356
|
-
recordTransactionState(entry, response) {
|
|
357
|
-
if (response.type === "error") {
|
|
358
|
-
this.resetFailedTransaction(entry);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (entry.request.type === "begin") {
|
|
362
|
-
this.activeTransactionClient = entry.clientId;
|
|
363
|
-
this.armTransactionIdleTimer(entry.clientId);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (this.activeTransactionClient === entry.clientId) {
|
|
367
|
-
this.armTransactionIdleTimer(entry.clientId);
|
|
368
|
-
}
|
|
369
|
-
if (this.activeTransactionClient === entry.clientId && (entry.request.type === "commit" || entry.request.type === "rollback")) {
|
|
370
|
-
this.activeTransactionClient = null;
|
|
371
|
-
this.clearTransactionIdleTimer();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
resetFailedTransaction(entry) {
|
|
375
|
-
if (this.activeTransactionClient === entry.clientId) {
|
|
376
|
-
this.activeTransactionClient = null;
|
|
377
|
-
this.clearTransactionIdleTimer();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
armTransactionIdleTimer(clientId) {
|
|
381
|
-
this.clearTransactionIdleTimer();
|
|
382
|
-
this.transactionIdleTimer = setTimeout(() => {
|
|
383
|
-
void this.reclaimClientNow(clientId, "transaction-idle-timeout");
|
|
384
|
-
}, this.transactionIdleTimeoutMs);
|
|
385
|
-
}
|
|
386
|
-
clearTransactionIdleTimer() {
|
|
387
|
-
if (this.transactionIdleTimer) {
|
|
388
|
-
clearTimeout(this.transactionIdleTimer);
|
|
389
|
-
this.transactionIdleTimer = null;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async reclaimClientNow(clientId, reason) {
|
|
393
|
-
if (this.reclaiming || this.activeTransactionClient !== clientId) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
this.reclaiming = true;
|
|
397
|
-
this.clearTransactionIdleTimer();
|
|
398
|
-
this.activeTransactionClient = null;
|
|
399
|
-
this.abortedClients.add(clientId);
|
|
400
|
-
this.rejectQueuedClientRequests(clientId, reason);
|
|
401
|
-
try {
|
|
402
|
-
const response = await this.inner.send({ id: 0, type: "rollback" });
|
|
403
|
-
if (response.type === "error") {
|
|
404
|
-
}
|
|
405
|
-
} catch {
|
|
406
|
-
} finally {
|
|
407
|
-
this.reclaiming = false;
|
|
408
|
-
if (!this.terminated && this.nextRunnableIndex() !== -1) {
|
|
409
|
-
void this.processQueue();
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
rejectQueuedClientRequests(clientId, reason) {
|
|
414
|
-
const keep = [];
|
|
415
|
-
for (const entry of this.queue) {
|
|
416
|
-
if (entry.clientId === clientId) {
|
|
417
|
-
entry.resolve({
|
|
418
|
-
id: entry.request.id,
|
|
419
|
-
type: "error",
|
|
420
|
-
message: `Transaction client was reclaimed: ${reason}`,
|
|
421
|
-
code: "TRANSACTION_ABORTED"
|
|
422
|
-
});
|
|
423
|
-
} else {
|
|
424
|
-
keep.push(entry);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
this.queue = keep;
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
function hasReclaimClient(bridge) {
|
|
431
|
-
return typeof bridge.reclaimClient === "function";
|
|
432
|
-
}
|
|
433
|
-
function createClientId() {
|
|
434
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
435
|
-
return crypto.randomUUID();
|
|
436
|
-
}
|
|
437
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/adapters/sqlite-wasm-adapter.ts
|
|
441
|
-
var warnedSharedWorkerDeprecated = false;
|
|
442
|
-
var SqliteWasmAdapter = class {
|
|
443
|
-
bridge = null;
|
|
444
|
-
opened = false;
|
|
445
|
-
mutex = new Mutex();
|
|
446
|
-
injectedBridge;
|
|
447
|
-
workerUrl;
|
|
448
|
-
sharedWorkerUrl;
|
|
449
|
-
workerResponseTimeoutMs;
|
|
450
|
-
dbName;
|
|
451
|
-
emitter;
|
|
452
|
-
tabSession = null;
|
|
453
|
-
emitNonPersistentDiagnostic;
|
|
454
|
-
storageOpenState = null;
|
|
455
|
-
/** Retained so a follower promoted to leader can re-open its own worker. */
|
|
456
|
-
schema = null;
|
|
457
|
-
promoting = false;
|
|
458
|
-
constructor(options = {}) {
|
|
459
|
-
this.injectedBridge = options.bridge;
|
|
460
|
-
this.workerUrl = options.workerUrl;
|
|
461
|
-
this.sharedWorkerUrl = options.sharedWorkerUrl;
|
|
462
|
-
this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
|
|
463
|
-
this.dbName = options.dbName ?? "kora-db";
|
|
464
|
-
this.emitter = options.emitter;
|
|
465
|
-
this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
|
|
466
|
-
}
|
|
467
|
-
async open(schema) {
|
|
468
|
-
if (this.opened) return;
|
|
469
|
-
const ddlStatements = generateFullDDL(schema);
|
|
470
|
-
if (this.injectedBridge) {
|
|
471
|
-
this.bridge = this.injectedBridge;
|
|
472
|
-
const response = await this.openCurrentBridge(ddlStatements);
|
|
473
|
-
this.reportStorageMode(response.data);
|
|
474
|
-
this.opened = true;
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (this.workerUrl) {
|
|
478
|
-
this.warnSharedWorkerDeprecated();
|
|
479
|
-
await this.openDurableWorkerBridge(schema);
|
|
480
|
-
const response = await this.openCurrentBridge(ddlStatements);
|
|
481
|
-
this.reportStorageMode(response.data);
|
|
482
|
-
this.opened = true;
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
throw new AdapterError(
|
|
486
|
-
'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
async openDurableWorkerBridge(schema) {
|
|
490
|
-
if (!this.workerUrl) {
|
|
491
|
-
throw new AdapterError("Durable SQLite WASM storage requires workerUrl.");
|
|
492
|
-
}
|
|
493
|
-
this.schema = schema;
|
|
494
|
-
this.tabSession = await acquireTabStorageSession(this.dbName, {
|
|
495
|
-
onPromote: () => {
|
|
496
|
-
void this.promoteToLeader();
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
const { WebWorkerBridge } = await import("./sqlite-wasm-channel-GGHA2BXU.js");
|
|
500
|
-
if (this.tabSession.role === "leader") {
|
|
501
|
-
const workerBridge = new TransactionSerializingWorkerBridge(
|
|
502
|
-
new WebWorkerBridge(this.workerUrl, this.workerResponseTimeoutMs)
|
|
503
|
-
);
|
|
504
|
-
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
505
|
-
this.bridge = workerBridge;
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const followerBridge = new FollowerBroadcastBridge(
|
|
509
|
-
this.tabSession.channelName,
|
|
510
|
-
this.workerResponseTimeoutMs
|
|
511
|
-
);
|
|
512
|
-
this.bridge = followerBridge;
|
|
513
|
-
this.emitter?.emit({
|
|
514
|
-
type: "store:db-name-collision",
|
|
515
|
-
dbName: this.dbName,
|
|
516
|
-
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.`
|
|
517
|
-
});
|
|
518
|
-
await followerBridge.waitForLeader();
|
|
519
|
-
}
|
|
520
|
-
async openCurrentBridge(ddlStatements) {
|
|
521
|
-
const response = await this.sendRequest({
|
|
522
|
-
id: 0,
|
|
523
|
-
type: "open",
|
|
524
|
-
ddlStatements,
|
|
525
|
-
dbName: this.dbName
|
|
526
|
-
});
|
|
527
|
-
if (response.type === "error") {
|
|
528
|
-
throw new AdapterError(`Failed to open database: ${response.message}`, {
|
|
529
|
-
code: response.code,
|
|
530
|
-
dbName: this.dbName
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
return response;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Surface a `store:opfs-unavailable` diagnostic when the worker reported that
|
|
537
|
-
* persistence silently degraded to an in-memory database, so a data-loss
|
|
538
|
-
* condition is observable instead of failing silently. A persistent open (or a
|
|
539
|
-
* bridge that does not report a mode, e.g. the Node mock) emits nothing.
|
|
540
|
-
*/
|
|
541
|
-
reportStorageMode(data) {
|
|
542
|
-
this.storageOpenState = parseStorageOpenState(data);
|
|
543
|
-
if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (this.storageOpenState.persistent === false) {
|
|
547
|
-
const reason = this.storageOpenState.fallbackReason ?? "unsupported";
|
|
548
|
-
this.emitter.emit({
|
|
549
|
-
type: "store:opfs-unavailable",
|
|
550
|
-
dbName: this.dbName,
|
|
551
|
-
reason,
|
|
552
|
-
message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
getStorageOpenState() {
|
|
557
|
-
return this.storageOpenState;
|
|
558
|
-
}
|
|
559
|
-
warnSharedWorkerDeprecated() {
|
|
560
|
-
if (!this.sharedWorkerUrl || warnedSharedWorkerDeprecated) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
warnedSharedWorkerDeprecated = true;
|
|
564
|
-
console.warn(
|
|
565
|
-
"[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."
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
async close() {
|
|
569
|
-
if (!this.bridge) return;
|
|
570
|
-
try {
|
|
571
|
-
await this.sendRequest({ id: 0, type: "close" });
|
|
572
|
-
} finally {
|
|
573
|
-
this.tabSession?.stopRelay?.();
|
|
574
|
-
this.tabSession?.cancelPromotionWatch?.();
|
|
575
|
-
if (this.tabSession?.releaseLock) {
|
|
576
|
-
await this.tabSession.releaseLock();
|
|
577
|
-
}
|
|
578
|
-
this.tabSession = null;
|
|
579
|
-
this.bridge.terminate();
|
|
580
|
-
this.bridge = null;
|
|
581
|
-
this.opened = false;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Promotes a follower to leader after the previous leader released the storage
|
|
586
|
-
* lock (its tab closed or crashed). The lock is only granted once the old leader
|
|
587
|
-
* is gone, so opening our own worker here is safe from the OPFS single-writer
|
|
588
|
-
* rule. In-flight follower RPCs are rejected and left to the caller / reactive
|
|
589
|
-
* layer to retry against the new leader.
|
|
590
|
-
*/
|
|
591
|
-
async promoteToLeader() {
|
|
592
|
-
if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
this.promoting = true;
|
|
596
|
-
const { WebWorkerBridge } = await import("./sqlite-wasm-channel-GGHA2BXU.js");
|
|
597
|
-
const workerBridge = new TransactionSerializingWorkerBridge(
|
|
598
|
-
new WebWorkerBridge(this.workerUrl, this.workerResponseTimeoutMs)
|
|
599
|
-
);
|
|
600
|
-
const previousBridge = this.bridge;
|
|
601
|
-
this.bridge = workerBridge;
|
|
602
|
-
if (this.tabSession) {
|
|
603
|
-
this.tabSession.role = "leader";
|
|
604
|
-
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
605
|
-
}
|
|
606
|
-
previousBridge?.terminate();
|
|
607
|
-
const ddlStatements = generateFullDDL(this.schema);
|
|
608
|
-
await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
|
|
609
|
-
}
|
|
610
|
-
async execute(sql, params) {
|
|
611
|
-
this.guardOpen();
|
|
612
|
-
const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
|
|
613
|
-
if (response.type === "error") {
|
|
614
|
-
throw new AdapterError(`Execute failed: ${response.message}`, { sql, params });
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async query(sql, params) {
|
|
618
|
-
this.guardOpen();
|
|
619
|
-
const response = await this.sendRequest({ id: 0, type: "query", sql, params });
|
|
620
|
-
if (response.type === "error") {
|
|
621
|
-
throw new AdapterError(`Query failed: ${response.message}`, { sql, params });
|
|
622
|
-
}
|
|
623
|
-
return response.data ?? [];
|
|
624
|
-
}
|
|
625
|
-
async transaction(fn) {
|
|
626
|
-
this.guardOpen();
|
|
627
|
-
const release = await this.mutex.acquire();
|
|
628
|
-
try {
|
|
629
|
-
await this.sendChecked({ id: 0, type: "begin" }, "BEGIN transaction");
|
|
630
|
-
const tx = {
|
|
631
|
-
execute: async (sql, params) => {
|
|
632
|
-
const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
|
|
633
|
-
if (response.type === "error") {
|
|
634
|
-
throw new AdapterError(`Transaction execute failed: ${response.message}`, {
|
|
635
|
-
sql,
|
|
636
|
-
params
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
},
|
|
640
|
-
query: async (sql, params) => {
|
|
641
|
-
const response = await this.sendRequest({ id: 0, type: "query", sql, params });
|
|
642
|
-
if (response.type === "error") {
|
|
643
|
-
throw new AdapterError(`Transaction query failed: ${response.message}`, { sql, params });
|
|
644
|
-
}
|
|
645
|
-
return response.data ?? [];
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
await fn(tx);
|
|
649
|
-
await this.sendChecked({ id: 0, type: "commit" }, "COMMIT transaction");
|
|
650
|
-
} catch (error) {
|
|
651
|
-
try {
|
|
652
|
-
await this.sendRequest({ id: 0, type: "rollback" });
|
|
653
|
-
} catch {
|
|
654
|
-
}
|
|
655
|
-
throw error;
|
|
656
|
-
} finally {
|
|
657
|
-
release();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
async migrate(from, to, migration) {
|
|
661
|
-
this.guardOpen();
|
|
662
|
-
const release = await this.mutex.acquire();
|
|
663
|
-
try {
|
|
664
|
-
await this.sendChecked({ id: 0, type: "begin" }, "BEGIN migration");
|
|
665
|
-
for (const sql of migration.statements) {
|
|
666
|
-
const response = await this.sendRequest({ id: 0, type: "execute", sql });
|
|
667
|
-
if (response.type === "error") {
|
|
668
|
-
throw new AdapterError(`Migration from v${from} to v${to} failed: ${response.message}`, {
|
|
669
|
-
from,
|
|
670
|
-
to
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
await this.sendChecked({ id: 0, type: "commit" }, "COMMIT migration");
|
|
675
|
-
} catch (error) {
|
|
676
|
-
try {
|
|
677
|
-
await this.sendRequest({ id: 0, type: "rollback" });
|
|
678
|
-
} catch {
|
|
679
|
-
}
|
|
680
|
-
if (error instanceof AdapterError) throw error;
|
|
681
|
-
throw new AdapterError(
|
|
682
|
-
`Migration from v${from} to v${to} failed: ${error.message}`,
|
|
683
|
-
{ from, to }
|
|
684
|
-
);
|
|
685
|
-
} finally {
|
|
686
|
-
release();
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Export the database as a Uint8Array (for IndexedDB persistence).
|
|
691
|
-
* Only available when the database is open.
|
|
692
|
-
*/
|
|
693
|
-
async exportDatabase() {
|
|
694
|
-
this.guardOpen();
|
|
695
|
-
const response = await this.sendRequest({ id: 0, type: "export" });
|
|
696
|
-
if (response.type === "error") {
|
|
697
|
-
throw new AdapterError(`Export failed: ${response.message}`, {
|
|
698
|
-
code: response.code,
|
|
699
|
-
context: response.context
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
return response.data;
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Import a serialized database snapshot.
|
|
706
|
-
*/
|
|
707
|
-
async importDatabase(data) {
|
|
708
|
-
this.guardOpen();
|
|
709
|
-
const response = await this.sendRequest({ id: 0, type: "import", data });
|
|
710
|
-
if (response.type === "error") {
|
|
711
|
-
throw new AdapterError(`Import failed: ${response.message}`);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
guardOpen() {
|
|
715
|
-
if (!this.opened || !this.bridge) {
|
|
716
|
-
throw new StoreNotOpenError();
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
async sendRequest(request) {
|
|
720
|
-
const bridge = this.bridge;
|
|
721
|
-
if (!bridge) {
|
|
722
|
-
throw new StoreNotOpenError();
|
|
723
|
-
}
|
|
724
|
-
return bridge.send(request);
|
|
725
|
-
}
|
|
726
|
-
async sendChecked(request, description) {
|
|
727
|
-
const response = await this.sendRequest(request);
|
|
728
|
-
if (response.type === "error") {
|
|
729
|
-
throw new AdapterError(`${description} failed: ${response.message}`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
function parseStorageOpenState(data) {
|
|
734
|
-
if (typeof data !== "object" || data === null) {
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
const mode = data;
|
|
738
|
-
if (typeof mode.persistent !== "boolean") {
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
return {
|
|
742
|
-
persistent: mode.persistent,
|
|
743
|
-
mode: mode.persistent ? "opfs" : "memory",
|
|
744
|
-
...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {}
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
export {
|
|
749
|
-
SqliteWasmAdapter
|
|
750
|
-
};
|
|
751
|
-
//# sourceMappingURL=chunk-RJIEKEN4.js.map
|