@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
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BridgeTerminatedError,
|
|
3
|
+
LeaderUnresponsiveError,
|
|
4
|
+
NoLeaderError,
|
|
5
|
+
RequestAbortedError,
|
|
6
|
+
WorkerTimeoutError
|
|
7
|
+
} from "./chunk-SRHDJF5B.js";
|
|
8
|
+
|
|
9
|
+
// src/adapters/opfs-names.ts
|
|
10
|
+
var LEGACY_OPFS_POOL_NAME = "kora-opfs";
|
|
11
|
+
var OPFS_POOL_PREFIX = "kora-opfs-";
|
|
12
|
+
var MAX_POOL_SLUG_LENGTH = 48;
|
|
13
|
+
function fnv1a32Hex(value) {
|
|
14
|
+
let hash = 2166136261;
|
|
15
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
16
|
+
hash ^= value.charCodeAt(i);
|
|
17
|
+
hash = Math.imul(hash, 16777619);
|
|
18
|
+
}
|
|
19
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
20
|
+
}
|
|
21
|
+
function opfsPoolNameFor(dbName) {
|
|
22
|
+
const sanitized = dbName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
23
|
+
const slug = sanitized.slice(0, MAX_POOL_SLUG_LENGTH);
|
|
24
|
+
if (slug === dbName) {
|
|
25
|
+
return `${OPFS_POOL_PREFIX}${slug}`;
|
|
26
|
+
}
|
|
27
|
+
return `${OPFS_POOL_PREFIX}${slug}-${fnv1a32Hex(dbName)}`;
|
|
28
|
+
}
|
|
29
|
+
function opfsPoolDirectory(poolName) {
|
|
30
|
+
return `.${poolName}`;
|
|
31
|
+
}
|
|
32
|
+
function opfsPoolLockName(poolName) {
|
|
33
|
+
return `kora-opfs-pool:${poolName}`;
|
|
34
|
+
}
|
|
35
|
+
function leaderLockName(dbName) {
|
|
36
|
+
return `kora-leader-${dbName}`;
|
|
37
|
+
}
|
|
38
|
+
function storageChannelName(dbName) {
|
|
39
|
+
return `kora-storage-${dbName}`;
|
|
40
|
+
}
|
|
41
|
+
function opfsDatabaseFilename(dbName) {
|
|
42
|
+
const base = dbName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
43
|
+
return base.endsWith(".db") ? base : `${base}.db`;
|
|
44
|
+
}
|
|
45
|
+
function opfsPoolPath(dbName) {
|
|
46
|
+
return `/${opfsDatabaseFilename(dbName)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/multi-tab/tab-storage.ts
|
|
50
|
+
var RPC_REQUEST = "kora-worker-request";
|
|
51
|
+
var RPC_RESPONSE = "kora-worker-response";
|
|
52
|
+
var CLIENT_LEAVE = "kora-client-leave";
|
|
53
|
+
var LEADER_PING = "kora-leader-ping";
|
|
54
|
+
var LEADER_PONG = "kora-leader-pong";
|
|
55
|
+
var LEADER_HEARTBEAT = "kora-leader-heartbeat";
|
|
56
|
+
var DEFAULT_HEARTBEAT_MS = 1e3;
|
|
57
|
+
var MISSED_BEATS_UNRESPONSIVE = 3;
|
|
58
|
+
var MISSED_BEATS_ABSENT = 2;
|
|
59
|
+
var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
|
|
60
|
+
var DEDUP_CAPACITY = 512;
|
|
61
|
+
function isMultiTabStorageSupported() {
|
|
62
|
+
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
63
|
+
}
|
|
64
|
+
async function acquireTabStorageSession(dbName, options) {
|
|
65
|
+
const channelName = storageChannelName(dbName);
|
|
66
|
+
const lockName = leaderLockName(dbName);
|
|
67
|
+
if (!isMultiTabStorageSupported()) {
|
|
68
|
+
return { role: "leader", channelName };
|
|
69
|
+
}
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
let releaseHeld;
|
|
72
|
+
void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
|
|
73
|
+
if (lock === null) {
|
|
74
|
+
resolve(startFollowerSession(channelName, lockName, options));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
resolve({
|
|
78
|
+
role: "leader",
|
|
79
|
+
channelName,
|
|
80
|
+
releaseLock: async () => {
|
|
81
|
+
releaseHeld?.();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return new Promise((release) => {
|
|
85
|
+
releaseHeld = release;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function startFollowerSession(channelName, lockName, options) {
|
|
91
|
+
let promoted = false;
|
|
92
|
+
let releasePromotedLock;
|
|
93
|
+
const abort = new AbortController();
|
|
94
|
+
void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
|
|
95
|
+
promoted = true;
|
|
96
|
+
options?.onPromote?.();
|
|
97
|
+
return new Promise((release) => {
|
|
98
|
+
releasePromotedLock = release;
|
|
99
|
+
});
|
|
100
|
+
}).catch(() => {
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
role: "follower",
|
|
104
|
+
channelName,
|
|
105
|
+
cancelPromotionWatch: () => {
|
|
106
|
+
if (promoted) {
|
|
107
|
+
releasePromotedLock?.();
|
|
108
|
+
} else {
|
|
109
|
+
abort.abort();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createRequestDeduper() {
|
|
115
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
116
|
+
const completed = /* @__PURE__ */ new Map();
|
|
117
|
+
return {
|
|
118
|
+
run(requestId, start) {
|
|
119
|
+
const done = completed.get(requestId);
|
|
120
|
+
if (done) return Promise.resolve(done);
|
|
121
|
+
if (inflight.has(requestId)) return null;
|
|
122
|
+
const promise = start().then((response) => {
|
|
123
|
+
inflight.delete(requestId);
|
|
124
|
+
completed.set(requestId, response);
|
|
125
|
+
if (completed.size > DEDUP_CAPACITY) {
|
|
126
|
+
const oldest = completed.keys().next().value;
|
|
127
|
+
if (oldest !== void 0) completed.delete(oldest);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
});
|
|
131
|
+
inflight.set(requestId, promise);
|
|
132
|
+
return promise;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function serveChannel(channel, bridge, source, heartbeatMs) {
|
|
137
|
+
const epoch = createClientId();
|
|
138
|
+
const dedup = createRequestDeduper();
|
|
139
|
+
const beat = () => {
|
|
140
|
+
const message = { type: LEADER_HEARTBEAT, epoch, source };
|
|
141
|
+
channel.postMessage(message);
|
|
142
|
+
};
|
|
143
|
+
const respond = (requestId, response) => {
|
|
144
|
+
const msg = { type: RPC_RESPONSE, requestId, response };
|
|
145
|
+
channel.postMessage(msg);
|
|
146
|
+
};
|
|
147
|
+
const onMessage = (event) => {
|
|
148
|
+
const data = event.data;
|
|
149
|
+
if (data?.type === LEADER_PING) {
|
|
150
|
+
channel.postMessage({ type: LEADER_PONG });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (data?.type === CLIENT_LEAVE) {
|
|
154
|
+
if (hasReclaimClient(bridge)) {
|
|
155
|
+
bridge.reclaimClient(data.clientId, "client-left");
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (data?.type !== RPC_REQUEST) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (data.request.type === "close" || data.request.type === "destroy" || data.request.type === "serve") {
|
|
163
|
+
respond(data.requestId, { id: data.request.id, type: "success" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const pending = dedup.run(
|
|
167
|
+
data.requestId,
|
|
168
|
+
() => bridge.send(data.request, data.clientId).catch(
|
|
169
|
+
(error) => ({
|
|
170
|
+
id: data.request.id,
|
|
171
|
+
type: "error",
|
|
172
|
+
message: error instanceof Error ? error.message : "Worker RPC failed",
|
|
173
|
+
code: "LEADER_RPC_ERROR"
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
void pending?.then((response) => respond(data.requestId, response));
|
|
178
|
+
};
|
|
179
|
+
channel.addEventListener("message", onMessage);
|
|
180
|
+
const interval = heartbeatMs > 0 ? setInterval(beat, heartbeatMs) : void 0;
|
|
181
|
+
if (heartbeatMs > 0) beat();
|
|
182
|
+
return {
|
|
183
|
+
beat,
|
|
184
|
+
stop: () => {
|
|
185
|
+
if (interval !== void 0) clearInterval(interval);
|
|
186
|
+
channel.removeEventListener("message", onMessage);
|
|
187
|
+
channel.close();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function startWorkerRpcRelay(channelName, bridge, options = {}) {
|
|
192
|
+
return serveChannel(
|
|
193
|
+
new BroadcastChannel(channelName),
|
|
194
|
+
bridge,
|
|
195
|
+
"worker",
|
|
196
|
+
options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
var FollowerBroadcastBridge = class {
|
|
200
|
+
channel;
|
|
201
|
+
clientId = createClientId();
|
|
202
|
+
onPageHide = () => {
|
|
203
|
+
this.leaveLeader();
|
|
204
|
+
};
|
|
205
|
+
pending = /* @__PURE__ */ new Map();
|
|
206
|
+
timeoutMs;
|
|
207
|
+
heartbeatMs;
|
|
208
|
+
terminated = false;
|
|
209
|
+
lastHeardAt = 0;
|
|
210
|
+
heardSinceWatch = false;
|
|
211
|
+
watchdog;
|
|
212
|
+
/**
|
|
213
|
+
* @param channelName - Storage channel of the database
|
|
214
|
+
* @param timeoutMs - Hard ceiling per request (default 30s)
|
|
215
|
+
* @param heartbeatMs - Expected leader heartbeat interval, also the watchdog tick
|
|
216
|
+
*/
|
|
217
|
+
constructor(channelName, timeoutMs = 3e4, heartbeatMs = DEFAULT_HEARTBEAT_MS) {
|
|
218
|
+
this.timeoutMs = timeoutMs;
|
|
219
|
+
this.heartbeatMs = Math.max(10, Math.min(heartbeatMs, timeoutMs));
|
|
220
|
+
this.channel = new BroadcastChannel(channelName);
|
|
221
|
+
if (typeof addEventListener === "function") {
|
|
222
|
+
addEventListener("pagehide", this.onPageHide);
|
|
223
|
+
}
|
|
224
|
+
this.channel.addEventListener("message", (event) => {
|
|
225
|
+
const data = event.data;
|
|
226
|
+
if (data?.type === LEADER_HEARTBEAT || data?.type === LEADER_PONG || data?.type === RPC_RESPONSE) {
|
|
227
|
+
this.markHeard();
|
|
228
|
+
}
|
|
229
|
+
if (data?.type !== RPC_RESPONSE) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const entry = this.pending.get(data.requestId);
|
|
233
|
+
if (entry) {
|
|
234
|
+
this.pending.delete(data.requestId);
|
|
235
|
+
entry.resolve(data.response);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
241
|
+
* or `false` if none answers within the budget. The adapter calls this before
|
|
242
|
+
* its first RPC so a follower created before a leader relay is live retries the
|
|
243
|
+
* handshake instead of firing into the void.
|
|
244
|
+
*/
|
|
245
|
+
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
246
|
+
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
247
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
248
|
+
if (this.terminated) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (await this.pingLeader(perAttempt)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
/** Sends one liveness ping and resolves whether a leader answered in time. */
|
|
258
|
+
pingLeader(timeoutMs) {
|
|
259
|
+
if (this.terminated) {
|
|
260
|
+
return Promise.resolve(false);
|
|
261
|
+
}
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
const onPong = (event) => {
|
|
264
|
+
if (event.data?.type === LEADER_PONG) {
|
|
265
|
+
cleanup();
|
|
266
|
+
resolve(true);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const cleanup = () => {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
this.channel.removeEventListener("message", onPong);
|
|
272
|
+
};
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
cleanup();
|
|
275
|
+
resolve(false);
|
|
276
|
+
}, timeoutMs);
|
|
277
|
+
this.channel.addEventListener("message", onPong);
|
|
278
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async send(request, _clientId, options = {}) {
|
|
282
|
+
if (this.terminated) {
|
|
283
|
+
return {
|
|
284
|
+
id: request.id,
|
|
285
|
+
type: "error",
|
|
286
|
+
message: "Follower bridge terminated",
|
|
287
|
+
code: "BRIDGE_TERMINATED"
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const requestId = request.requestId ?? createClientId();
|
|
291
|
+
const signal = options.signal;
|
|
292
|
+
if (signal?.aborted) {
|
|
293
|
+
throw new RequestAbortedError(request.type, requestId);
|
|
294
|
+
}
|
|
295
|
+
const msg = {
|
|
296
|
+
type: RPC_REQUEST,
|
|
297
|
+
requestId,
|
|
298
|
+
clientId: this.clientId,
|
|
299
|
+
request: { ...request, requestId }
|
|
300
|
+
};
|
|
301
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
let timer;
|
|
304
|
+
const onAbort = () => {
|
|
305
|
+
settle(() => reject(new RequestAbortedError(request.type, requestId)));
|
|
306
|
+
};
|
|
307
|
+
const settle = (fn) => {
|
|
308
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
309
|
+
signal?.removeEventListener("abort", onAbort);
|
|
310
|
+
this.pending.delete(requestId);
|
|
311
|
+
this.stopWatchdogIfIdle();
|
|
312
|
+
fn();
|
|
313
|
+
};
|
|
314
|
+
if (Number.isFinite(timeoutMs)) {
|
|
315
|
+
timer = setTimeout(() => {
|
|
316
|
+
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, timeoutMs)));
|
|
317
|
+
}, timeoutMs);
|
|
318
|
+
}
|
|
319
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
320
|
+
this.pending.set(requestId, {
|
|
321
|
+
type: request.type,
|
|
322
|
+
resolve: (response) => settle(() => resolve(response)),
|
|
323
|
+
reject: (error) => settle(() => reject(error))
|
|
324
|
+
});
|
|
325
|
+
this.startWatchdog();
|
|
326
|
+
this.channel.postMessage(msg);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
terminate() {
|
|
330
|
+
if (this.terminated) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.terminated = true;
|
|
334
|
+
this.leaveLeader();
|
|
335
|
+
if (typeof removeEventListener === "function") {
|
|
336
|
+
removeEventListener("pagehide", this.onPageHide);
|
|
337
|
+
}
|
|
338
|
+
this.channel.close();
|
|
339
|
+
for (const [, entry] of [...this.pending]) {
|
|
340
|
+
entry.reject(new BridgeTerminatedError(entry.type, "follower bridge terminated"));
|
|
341
|
+
}
|
|
342
|
+
this.pending.clear();
|
|
343
|
+
this.stopWatchdog();
|
|
344
|
+
}
|
|
345
|
+
markHeard() {
|
|
346
|
+
this.lastHeardAt = Date.now();
|
|
347
|
+
this.heardSinceWatch = true;
|
|
348
|
+
}
|
|
349
|
+
startWatchdog() {
|
|
350
|
+
if (this.watchdog !== void 0) return;
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
this.heardSinceWatch = now - this.lastHeardAt < this.heartbeatMs * 2;
|
|
353
|
+
if (!this.heardSinceWatch) this.lastHeardAt = now;
|
|
354
|
+
this.watchdog = setInterval(() => this.checkLiveness(), this.heartbeatMs);
|
|
355
|
+
}
|
|
356
|
+
stopWatchdogIfIdle() {
|
|
357
|
+
if (this.pending.size === 0) this.stopWatchdog();
|
|
358
|
+
}
|
|
359
|
+
stopWatchdog() {
|
|
360
|
+
if (this.watchdog !== void 0) {
|
|
361
|
+
clearInterval(this.watchdog);
|
|
362
|
+
this.watchdog = void 0;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
checkLiveness() {
|
|
366
|
+
if (this.pending.size === 0) {
|
|
367
|
+
this.stopWatchdog();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const silentMs = Date.now() - this.lastHeardAt;
|
|
371
|
+
const heard = this.heardSinceWatch;
|
|
372
|
+
const limit = this.heartbeatMs * (heard ? MISSED_BEATS_UNRESPONSIVE : MISSED_BEATS_ABSENT);
|
|
373
|
+
if (silentMs >= limit) {
|
|
374
|
+
for (const [requestId, entry] of [...this.pending]) {
|
|
375
|
+
entry.reject(
|
|
376
|
+
heard ? new LeaderUnresponsiveError(`follower-rpc:${entry.type}`, silentMs, requestId) : new NoLeaderError(`follower-rpc:${entry.type}`)
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (silentMs >= this.heartbeatMs) {
|
|
382
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
leaveLeader() {
|
|
386
|
+
try {
|
|
387
|
+
const message = {
|
|
388
|
+
type: CLIENT_LEAVE,
|
|
389
|
+
clientId: this.clientId
|
|
390
|
+
};
|
|
391
|
+
this.channel.postMessage(message);
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
var TransactionSerializingWorkerBridge = class {
|
|
397
|
+
inner;
|
|
398
|
+
leaderClientId = createClientId();
|
|
399
|
+
transactionIdleTimeoutMs;
|
|
400
|
+
queue = [];
|
|
401
|
+
activeTransactionClient = null;
|
|
402
|
+
abortedClients = /* @__PURE__ */ new Set();
|
|
403
|
+
transactionIdleTimer = null;
|
|
404
|
+
reclaiming = false;
|
|
405
|
+
processing = false;
|
|
406
|
+
terminated = false;
|
|
407
|
+
constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
|
|
408
|
+
this.inner = inner;
|
|
409
|
+
this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
|
|
410
|
+
}
|
|
411
|
+
send(request, clientId = this.leaderClientId) {
|
|
412
|
+
if (this.terminated) {
|
|
413
|
+
return Promise.resolve({
|
|
414
|
+
id: request.id,
|
|
415
|
+
type: "error",
|
|
416
|
+
message: "Worker has been terminated",
|
|
417
|
+
code: "WORKER_TERMINATED"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (this.abortedClients.has(clientId)) {
|
|
421
|
+
if (request.type === "begin") {
|
|
422
|
+
this.abortedClients.delete(clientId);
|
|
423
|
+
} else {
|
|
424
|
+
return Promise.resolve({
|
|
425
|
+
id: request.id,
|
|
426
|
+
type: "error",
|
|
427
|
+
message: "Previous transaction was aborted because the client stopped sending requests.",
|
|
428
|
+
code: "TRANSACTION_ABORTED"
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
this.queue.push({ clientId, request, resolve, reject });
|
|
434
|
+
void this.processQueue();
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
terminate() {
|
|
438
|
+
if (this.terminated) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
this.terminated = true;
|
|
442
|
+
this.clearTransactionIdleTimer();
|
|
443
|
+
this.inner.terminate();
|
|
444
|
+
const pending = this.queue.splice(0);
|
|
445
|
+
for (const entry of pending) {
|
|
446
|
+
entry.reject(new BridgeTerminatedError(entry.request.type, "worker terminated"));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
reclaimClient(clientId, reason) {
|
|
450
|
+
if (this.terminated) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
void this.reclaimClientNow(clientId, reason);
|
|
454
|
+
}
|
|
455
|
+
async processQueue() {
|
|
456
|
+
if (this.processing || this.reclaiming) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.processing = true;
|
|
460
|
+
try {
|
|
461
|
+
while (!this.terminated) {
|
|
462
|
+
const index = this.nextRunnableIndex();
|
|
463
|
+
if (index === -1) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const [entry] = this.queue.splice(index, 1);
|
|
467
|
+
if (!entry) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const response = await this.inner.send(entry.request);
|
|
472
|
+
this.recordTransactionState(entry, response);
|
|
473
|
+
entry.resolve(response);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
this.resetFailedTransaction(entry);
|
|
476
|
+
entry.reject(error instanceof Error ? error : new Error(String(error)));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
this.processing = false;
|
|
481
|
+
if (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {
|
|
482
|
+
void this.processQueue();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
nextRunnableIndex() {
|
|
487
|
+
if (this.queue.length === 0) {
|
|
488
|
+
return -1;
|
|
489
|
+
}
|
|
490
|
+
if (this.activeTransactionClient === null) {
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
return this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient);
|
|
494
|
+
}
|
|
495
|
+
recordTransactionState(entry, response) {
|
|
496
|
+
if (response.type === "error") {
|
|
497
|
+
this.resetFailedTransaction(entry);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (entry.request.type === "begin") {
|
|
501
|
+
this.activeTransactionClient = entry.clientId;
|
|
502
|
+
this.armTransactionIdleTimer(entry.clientId);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (this.activeTransactionClient === entry.clientId) {
|
|
506
|
+
this.armTransactionIdleTimer(entry.clientId);
|
|
507
|
+
}
|
|
508
|
+
if (this.activeTransactionClient === entry.clientId && (entry.request.type === "commit" || entry.request.type === "rollback")) {
|
|
509
|
+
this.activeTransactionClient = null;
|
|
510
|
+
this.clearTransactionIdleTimer();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
resetFailedTransaction(entry) {
|
|
514
|
+
if (this.activeTransactionClient === entry.clientId) {
|
|
515
|
+
this.activeTransactionClient = null;
|
|
516
|
+
this.clearTransactionIdleTimer();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
armTransactionIdleTimer(clientId) {
|
|
520
|
+
this.clearTransactionIdleTimer();
|
|
521
|
+
this.transactionIdleTimer = setTimeout(() => {
|
|
522
|
+
void this.reclaimClientNow(clientId, "transaction-idle-timeout");
|
|
523
|
+
}, this.transactionIdleTimeoutMs);
|
|
524
|
+
}
|
|
525
|
+
clearTransactionIdleTimer() {
|
|
526
|
+
if (this.transactionIdleTimer) {
|
|
527
|
+
clearTimeout(this.transactionIdleTimer);
|
|
528
|
+
this.transactionIdleTimer = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async reclaimClientNow(clientId, reason) {
|
|
532
|
+
if (this.reclaiming || this.activeTransactionClient !== clientId) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.reclaiming = true;
|
|
536
|
+
this.clearTransactionIdleTimer();
|
|
537
|
+
this.activeTransactionClient = null;
|
|
538
|
+
this.abortedClients.add(clientId);
|
|
539
|
+
this.rejectQueuedClientRequests(clientId, reason);
|
|
540
|
+
try {
|
|
541
|
+
const response = await this.inner.send({ id: 0, type: "rollback" });
|
|
542
|
+
if (response.type === "error") {
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
} finally {
|
|
546
|
+
this.reclaiming = false;
|
|
547
|
+
if (!this.terminated && this.nextRunnableIndex() !== -1) {
|
|
548
|
+
void this.processQueue();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
rejectQueuedClientRequests(clientId, reason) {
|
|
553
|
+
const keep = [];
|
|
554
|
+
for (const entry of this.queue) {
|
|
555
|
+
if (entry.clientId === clientId) {
|
|
556
|
+
entry.resolve({
|
|
557
|
+
id: entry.request.id,
|
|
558
|
+
type: "error",
|
|
559
|
+
message: `Transaction client was reclaimed: ${reason}`,
|
|
560
|
+
code: "TRANSACTION_ABORTED"
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
keep.push(entry);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
this.queue = keep;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
function hasReclaimClient(bridge) {
|
|
570
|
+
return typeof bridge.reclaimClient === "function";
|
|
571
|
+
}
|
|
572
|
+
function createClientId() {
|
|
573
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
574
|
+
return crypto.randomUUID();
|
|
575
|
+
}
|
|
576
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export {
|
|
580
|
+
LEGACY_OPFS_POOL_NAME,
|
|
581
|
+
opfsPoolNameFor,
|
|
582
|
+
opfsPoolDirectory,
|
|
583
|
+
opfsPoolLockName,
|
|
584
|
+
leaderLockName,
|
|
585
|
+
opfsPoolPath,
|
|
586
|
+
acquireTabStorageSession,
|
|
587
|
+
startWorkerRpcRelay,
|
|
588
|
+
FollowerBroadcastBridge,
|
|
589
|
+
TransactionSerializingWorkerBridge
|
|
590
|
+
};
|
|
591
|
+
//# sourceMappingURL=chunk-5JMGMPUE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/opfs-names.ts","../src/multi-tab/tab-storage.ts"],"sourcesContent":["/**\n * Names shared by the main thread and the SQLite worker for OPFS pools, Web Locks\n * and database files. Pure functions, so ownership rules are testable in Node.\n *\n * Ownership model (W8a, NEW-STORE-5): one OPFS SyncAccessHandle pool per\n * database. The tab leader lock (`kora-leader-<db>`) already guarantees one\n * leader tab per database, so a pool per database means two different databases\n * on one origin (per-user databases, two Kora apps) never contend for one pool.\n */\n\n/** Pool name every database used before W8a (beta.12 and earlier): one per origin. */\nexport const LEGACY_OPFS_POOL_NAME = 'kora-opfs'\n\n/** Prefix of the per-database pool names. */\nexport const OPFS_POOL_PREFIX = 'kora-opfs-'\n\n/**\n * Longest readable database slug kept in a pool name. Pool names become OPFS\n * directory names (`.<pool>`) and VFS names, so they stay short and portable.\n */\nconst MAX_POOL_SLUG_LENGTH = 48\n\n/**\n * 32-bit FNV-1a hash as 8 hex digits. Used only to keep pool names unique when\n * a database name had to be sanitized or truncated; not a security primitive.\n */\nexport function fnv1a32Hex(value: string): string {\n\tlet hash = 0x811c9dc5\n\tfor (let i = 0; i < value.length; i += 1) {\n\t\thash ^= value.charCodeAt(i)\n\t\thash = Math.imul(hash, 0x01000193)\n\t}\n\treturn (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n/**\n * The OPFS SAH pool that owns `dbName`. Database names that are already safe\n * map to `kora-opfs-<dbName>`; anything sanitized or truncated gets a hash suffix\n * so two distinct names can never share a pool.\n *\n * @example opfsPoolNameFor('school__user_alice') === 'kora-opfs-school__user_alice'\n */\nexport function opfsPoolNameFor(dbName: string): string {\n\tconst sanitized = dbName.replace(/[^a-zA-Z0-9_-]/g, '_')\n\tconst slug = sanitized.slice(0, MAX_POOL_SLUG_LENGTH)\n\tif (slug === dbName) {\n\t\treturn `${OPFS_POOL_PREFIX}${slug}`\n\t}\n\treturn `${OPFS_POOL_PREFIX}${slug}-${fnv1a32Hex(dbName)}`\n}\n\n/** OPFS directory sqlite-wasm uses for a pool (its default is `.` + pool name). */\nexport function opfsPoolDirectory(poolName: string): string {\n\treturn `.${poolName}`\n}\n\n/**\n * Web Lock held by the worker that owns a pool, for the pool's entire life. A\n * new owner (a promoted tab, a reopened database) waits on it instead of racing\n * the previous worker for the file handles.\n */\nexport function opfsPoolLockName(poolName: string): string {\n\treturn `kora-opfs-pool:${poolName}`\n}\n\n/** Tab leader lock for a database (one leader tab per database per origin). */\nexport function leaderLockName(dbName: string): string {\n\treturn `kora-leader-${dbName}`\n}\n\n/** BroadcastChannel carrying follower RPC, heartbeats and pings for a database. */\nexport function storageChannelName(dbName: string): string {\n\treturn `kora-storage-${dbName}`\n}\n\n/** File name of a database inside its pool (unchanged from beta.12 for migration). */\nexport function opfsDatabaseFilename(dbName: string): string {\n\tconst base = dbName.replace(/[^a-zA-Z0-9._-]/g, '_')\n\treturn base.endsWith('.db') ? base : `${base}.db`\n}\n\n/** Absolute path under which the SAH pool tracks a database file. */\nexport function opfsPoolPath(dbName: string): string {\n\treturn `/${opfsDatabaseFilename(dbName)}`\n}\n","/**\n * Multi-tab SQLite storage coordination via `navigator.locks` and `BroadcastChannel`.\n *\n * One tab holds the exclusive `kora-leader-${dbName}` lock and owns the SQLite\n * worker. Other tabs send worker RPC over a named broadcast channel. In browsers\n * the leader's WORKER answers that channel itself ({@link startWorkerRpcRelay}),\n * so a hung leader main thread never blocks other tabs, and it pushes heartbeats\n * (also from inside long statements) that followers use to detect a hung or\n * frozen leader (NEW-STORE-9).\n */\n\nimport { leaderLockName, storageChannelName } from '../adapters/opfs-names'\nimport type {\n\tWorkerBridge,\n\tWorkerRequest,\n\tWorkerResponse,\n\tWorkerSendOptions,\n} from '../adapters/sqlite-wasm-channel'\nimport {\n\tBridgeTerminatedError,\n\tLeaderUnresponsiveError,\n\tNoLeaderError,\n\tRequestAbortedError,\n\tWorkerTimeoutError,\n} from '../errors'\n\nconst RPC_REQUEST = 'kora-worker-request'\nconst RPC_RESPONSE = 'kora-worker-response'\nconst CLIENT_LEAVE = 'kora-client-leave'\nconst LEADER_PING = 'kora-leader-ping'\nconst LEADER_PONG = 'kora-leader-pong'\nconst LEADER_HEARTBEAT = 'kora-leader-heartbeat'\n\n/** Default interval between leader heartbeats and follower watchdog ticks. */\nexport const DEFAULT_HEARTBEAT_MS = 1000\n/** Missed heartbeat intervals after which a follower gives up on a leader it heard. */\nconst MISSED_BEATS_UNRESPONSIVE = 3\n/** Silent intervals after which a follower that never heard a leader gives up. */\nconst MISSED_BEATS_ABSENT = 2\n/** Default idle budget for a client that owns an open transaction span. */\nconst DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 10_000\n/** Completed follower responses remembered for de-duplicating retried requests. */\nconst DEDUP_CAPACITY = 512\n\ninterface RpcRequestMessage {\n\ttype: typeof RPC_REQUEST\n\trequestId: string\n\tclientId: string\n\trequest: WorkerRequest\n}\n\ninterface RpcResponseMessage {\n\ttype: typeof RPC_RESPONSE\n\trequestId: string\n\tresponse: WorkerResponse\n}\n\ninterface ClientLeaveMessage {\n\ttype: typeof CLIENT_LEAVE\n\tclientId: string\n}\n\ninterface HeartbeatMessage {\n\ttype: typeof LEADER_HEARTBEAT\n\tepoch: string\n\tsource: 'main' | 'worker'\n}\n\ntype ChannelMessage =\n\t| RpcRequestMessage\n\t| RpcResponseMessage\n\t| ClientLeaveMessage\n\t| HeartbeatMessage\n\t| { type: typeof LEADER_PING }\n\t| { type: typeof LEADER_PONG }\n\ninterface ReclaimingWorkerBridge extends WorkerBridge {\n\treclaimClient(clientId: string, reason: string): void\n}\n\nexport type TabStorageRole = 'leader' | 'follower'\n\nexport interface AcquireTabStorageOptions {\n\t/**\n\t * Invoked when a follower is promoted to leader because the previous leader\n\t * released the lock (its tab closed, crashed, or suspended itself on freeze).\n\t * The adapter uses this to rebuild its bridge as a leader. Never fires for a\n\t * tab that started as leader.\n\t */\n\tonPromote?: () => void\n}\n\nexport interface TabStorageSession {\n\trole: TabStorageRole\n\tchannelName: string\n\t/** Leader only: release the navigator lock when closing the database. */\n\treleaseLock?: () => Promise<void>\n\t/** Leader only: stop the broadcast RPC relay or heartbeat. */\n\tstopRelay?: () => void\n\t/**\n\t * Follower only: cancels the queued lock request (or, once promoted, releases\n\t * the lock this tab won). Present so callers can tear the session down.\n\t */\n\tcancelPromotionWatch?: () => void\n}\n\n/**\n * Returns whether multi-tab coordination APIs exist in this runtime.\n */\nexport function isMultiTabStorageSupported(): boolean {\n\treturn (\n\t\ttypeof globalThis !== 'undefined' &&\n\t\ttypeof BroadcastChannel !== 'undefined' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.locks?.request === 'function'\n\t)\n}\n\n/**\n * Resolve leader vs follower for a database name.\n * Without lock APIs, every instance is treated as leader (single-tab / Node).\n *\n * A follower additionally queues a blocking request for the same lock. When the\n * current leader releases it (its tab closed, crashed, or suspended on freeze),\n * the browser grants the lock to this follower and\n * {@link AcquireTabStorageOptions.onPromote} fires so the adapter can rebuild\n * itself as the new leader. Kora never uses the Web Locks `steal` option: a\n * frozen leader's worker would still hold the OPFS file handles.\n */\nexport async function acquireTabStorageSession(\n\tdbName: string,\n\toptions?: AcquireTabStorageOptions,\n): Promise<TabStorageSession> {\n\tconst channelName = storageChannelName(dbName)\n\tconst lockName = leaderLockName(dbName)\n\n\tif (!isMultiTabStorageSupported()) {\n\t\treturn { role: 'leader', channelName }\n\t}\n\n\treturn new Promise<TabStorageSession>((resolve) => {\n\t\tlet releaseHeld: (() => void) | undefined\n\n\t\tvoid navigator.locks.request(lockName, { mode: 'exclusive', ifAvailable: true }, (lock) => {\n\t\t\tif (lock === null) {\n\t\t\t\tresolve(startFollowerSession(channelName, lockName, options))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresolve({\n\t\t\t\trole: 'leader',\n\t\t\t\tchannelName,\n\t\t\t\treleaseLock: async () => {\n\t\t\t\t\treleaseHeld?.()\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treturn new Promise<void>((release) => {\n\t\t\t\treleaseHeld = release\n\t\t\t})\n\t\t})\n\t})\n}\n\n/**\n * Builds a follower session and starts watching for promotion. The follower holds\n * a queued lock request; when it is finally granted (old leader gone), it becomes\n * the leader for the rest of its lifetime and notifies via `onPromote`.\n */\nfunction startFollowerSession(\n\tchannelName: string,\n\tlockName: string,\n\toptions?: AcquireTabStorageOptions,\n): TabStorageSession {\n\tlet promoted = false\n\tlet releasePromotedLock: (() => void) | undefined\n\tconst abort = new AbortController()\n\n\tvoid navigator.locks\n\t\t.request(lockName, { mode: 'exclusive', signal: abort.signal }, () => {\n\t\t\t// Reaching here means the previous leader released the lock and this tab\n\t\t\t// won it. Hold it (never-resolving promise) so this tab is now the leader.\n\t\t\tpromoted = true\n\t\t\toptions?.onPromote?.()\n\t\t\treturn new Promise<void>((release) => {\n\t\t\t\treleasePromotedLock = release\n\t\t\t})\n\t\t})\n\t\t.catch(() => {\n\t\t\t// AbortError when the tab closes before promotion. Nothing to do.\n\t\t})\n\n\treturn {\n\t\trole: 'follower',\n\t\tchannelName,\n\t\tcancelPromotionWatch: () => {\n\t\t\tif (promoted) {\n\t\t\t\treleasePromotedLock?.()\n\t\t\t} else {\n\t\t\t\tabort.abort()\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Answers follower RPC with a bounded response cache keyed by request id, so a\n * follower that retries a request it lost track of (leader looked unresponsive,\n * caller aborted) gets the original response instead of a second application.\n */\nfunction createRequestDeduper(): {\n\trun(requestId: string, start: () => Promise<WorkerResponse>): Promise<WorkerResponse> | null\n} {\n\tconst inflight = new Map<string, Promise<WorkerResponse>>()\n\tconst completed = new Map<string, WorkerResponse>()\n\treturn {\n\t\trun(requestId, start) {\n\t\t\tconst done = completed.get(requestId)\n\t\t\tif (done) return Promise.resolve(done)\n\t\t\t// Already running: the original response is broadcast when it lands.\n\t\t\tif (inflight.has(requestId)) return null\n\t\t\tconst promise = start().then((response) => {\n\t\t\t\tinflight.delete(requestId)\n\t\t\t\tcompleted.set(requestId, response)\n\t\t\t\tif (completed.size > DEDUP_CAPACITY) {\n\t\t\t\t\tconst oldest = completed.keys().next().value\n\t\t\t\t\tif (oldest !== undefined) completed.delete(oldest)\n\t\t\t\t}\n\t\t\t\treturn response\n\t\t\t})\n\t\t\tinflight.set(requestId, promise)\n\t\t\treturn promise\n\t\t},\n\t}\n}\n\n/** Options for the leader-side relays. */\nexport interface LeaderRelayOptions {\n\t/** Heartbeat interval; 0 disables heartbeats. */\n\theartbeatMs?: number\n}\n\n/** Handle returned by {@link startWorkerRpcRelay}. */\nexport interface WorkerRpcRelay {\n\t/** Push a heartbeat now (called from inside long statements). */\n\tbeat(): void\n\tstop(): void\n}\n\nfunction serveChannel(\n\tchannel: BroadcastChannel,\n\tbridge: WorkerBridge,\n\tsource: 'main' | 'worker',\n\theartbeatMs: number,\n): WorkerRpcRelay {\n\tconst epoch = createClientId()\n\tconst dedup = createRequestDeduper()\n\tconst beat = (): void => {\n\t\tconst message: HeartbeatMessage = { type: LEADER_HEARTBEAT, epoch, source }\n\t\tchannel.postMessage(message)\n\t}\n\tconst respond = (requestId: string, response: WorkerResponse): void => {\n\t\tconst msg: RpcResponseMessage = { type: RPC_RESPONSE, requestId, response }\n\t\tchannel.postMessage(msg)\n\t}\n\n\tconst onMessage = (event: MessageEvent<ChannelMessage>): void => {\n\t\tconst data = event.data\n\t\t// Answer liveness probes immediately, without touching the database.\n\t\tif (data?.type === LEADER_PING) {\n\t\t\tchannel.postMessage({ type: LEADER_PONG })\n\t\t\treturn\n\t\t}\n\t\tif (data?.type === CLIENT_LEAVE) {\n\t\t\tif (hasReclaimClient(bridge)) {\n\t\t\t\tbridge.reclaimClient(data.clientId, 'client-left')\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif (data?.type !== RPC_REQUEST) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\tdata.request.type === 'close' ||\n\t\t\tdata.request.type === 'destroy' ||\n\t\t\tdata.request.type === 'serve'\n\t\t) {\n\t\t\t// Only the owning tab may close or remove the database; a follower\n\t\t\t// leaving just stops sending requests.\n\t\t\trespond(data.requestId, { id: data.request.id, type: 'success' })\n\t\t\treturn\n\t\t}\n\n\t\tconst pending = dedup.run(data.requestId, () =>\n\t\t\tbridge.send(data.request, data.clientId).catch(\n\t\t\t\t(error: unknown): WorkerResponse => ({\n\t\t\t\t\tid: data.request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: error instanceof Error ? error.message : 'Worker RPC failed',\n\t\t\t\t\tcode: 'LEADER_RPC_ERROR',\n\t\t\t\t}),\n\t\t\t),\n\t\t)\n\t\tvoid pending?.then((response) => respond(data.requestId, response))\n\t}\n\n\tchannel.addEventListener('message', onMessage)\n\tconst interval = heartbeatMs > 0 ? setInterval(beat, heartbeatMs) : undefined\n\tif (heartbeatMs > 0) beat()\n\treturn {\n\t\tbeat,\n\t\tstop: () => {\n\t\t\tif (interval !== undefined) clearInterval(interval)\n\t\t\tchannel.removeEventListener('message', onMessage)\n\t\t\tchannel.close()\n\t\t},\n\t}\n}\n\n/**\n * Leader main thread: forward follower RPC to the worker bridge. Used where the\n * worker cannot host the relay itself (Node tests, custom bridges). Heartbeats\n * are off by default here; pass `heartbeatMs` to enable them.\n */\nexport function startLeaderRpcRelay(\n\tchannelName: string,\n\tbridge: WorkerBridge,\n\toptions: LeaderRelayOptions = {},\n): () => void {\n\treturn serveChannel(new BroadcastChannel(channelName), bridge, 'main', options.heartbeatMs ?? 0)\n\t\t.stop\n}\n\n/**\n * Leader WORKER: answer follower RPC, pings and heartbeats from inside the\n * dedicated SQLite worker, through the worker's transaction serializer. The\n * leader tab's main thread is not on this path, so a hung or busy main thread\n * does not stall other tabs (NEW-STORE-9).\n */\nexport function startWorkerRpcRelay(\n\tchannelName: string,\n\tbridge: WorkerBridge,\n\toptions: LeaderRelayOptions = {},\n): WorkerRpcRelay {\n\treturn serveChannel(\n\t\tnew BroadcastChannel(channelName),\n\t\tbridge,\n\t\t'worker',\n\t\toptions.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,\n\t)\n}\n\ninterface PendingFollowerRequest {\n\ttype: string\n\tresolve: (r: WorkerResponse) => void\n\treject: (e: Error) => void\n}\n\n/**\n * Follower tab: proxy {@link WorkerBridge} over BroadcastChannel to the leader.\n *\n * A single watchdog runs while requests are pending. Any message from the leader\n * (heartbeat, pong, response) is a sign of life; when the leader goes quiet the\n * watchdog pings it, and after {@link MISSED_BEATS_UNRESPONSIVE} silent intervals\n * it fails every pending request with a typed, retriable\n * {@link LeaderUnresponsiveError} (or {@link NoLeaderError} if no leader was\n * heard at all) instead of waiting out the full RPC timeout.\n */\nexport class FollowerBroadcastBridge implements WorkerBridge {\n\tprivate readonly channel: BroadcastChannel\n\tprivate readonly clientId = createClientId()\n\tprivate readonly onPageHide = (): void => {\n\t\tthis.leaveLeader()\n\t}\n\tprivate readonly pending = new Map<string, PendingFollowerRequest>()\n\tprivate readonly timeoutMs: number\n\tprivate readonly heartbeatMs: number\n\tprivate terminated = false\n\tprivate lastHeardAt = 0\n\tprivate heardSinceWatch = false\n\tprivate watchdog: ReturnType<typeof setInterval> | undefined\n\n\t/**\n\t * @param channelName - Storage channel of the database\n\t * @param timeoutMs - Hard ceiling per request (default 30s)\n\t * @param heartbeatMs - Expected leader heartbeat interval, also the watchdog tick\n\t */\n\tconstructor(channelName: string, timeoutMs = 30000, heartbeatMs = DEFAULT_HEARTBEAT_MS) {\n\t\tthis.timeoutMs = timeoutMs\n\t\tthis.heartbeatMs = Math.max(10, Math.min(heartbeatMs, timeoutMs))\n\t\tthis.channel = new BroadcastChannel(channelName)\n\t\tif (typeof addEventListener === 'function') {\n\t\t\taddEventListener('pagehide', this.onPageHide)\n\t\t}\n\t\tthis.channel.addEventListener('message', (event: MessageEvent<ChannelMessage>) => {\n\t\t\tconst data = event.data\n\t\t\tif (\n\t\t\t\tdata?.type === LEADER_HEARTBEAT ||\n\t\t\t\tdata?.type === LEADER_PONG ||\n\t\t\t\tdata?.type === RPC_RESPONSE\n\t\t\t) {\n\t\t\t\tthis.markHeard()\n\t\t\t}\n\t\t\tif (data?.type !== RPC_RESPONSE) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst entry = this.pending.get(data.requestId)\n\t\t\tif (entry) {\n\t\t\t\tthis.pending.delete(data.requestId)\n\t\t\t\tentry.resolve(data.response)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Readiness handshake: resolves `true` as soon as a live leader answers a ping,\n\t * or `false` if none answers within the budget. The adapter calls this before\n\t * its first RPC so a follower created before a leader relay is live retries the\n\t * handshake instead of firing into the void.\n\t */\n\tasync waitForLeader(timeoutMs = 3000, attempts = 3): Promise<boolean> {\n\t\tconst perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)))\n\t\tfor (let attempt = 0; attempt < attempts; attempt += 1) {\n\t\t\tif (this.terminated) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (await this.pingLeader(perAttempt)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t/** Sends one liveness ping and resolves whether a leader answered in time. */\n\tprivate pingLeader(timeoutMs: number): Promise<boolean> {\n\t\tif (this.terminated) {\n\t\t\treturn Promise.resolve(false)\n\t\t}\n\t\treturn new Promise<boolean>((resolve) => {\n\t\t\tconst onPong = (event: MessageEvent<{ type: string }>): void => {\n\t\t\t\tif (event.data?.type === LEADER_PONG) {\n\t\t\t\t\tcleanup()\n\t\t\t\t\tresolve(true)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tthis.channel.removeEventListener('message', onPong)\n\t\t\t}\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup()\n\t\t\t\tresolve(false)\n\t\t\t}, timeoutMs)\n\t\t\tthis.channel.addEventListener('message', onPong)\n\t\t\tthis.channel.postMessage({ type: LEADER_PING })\n\t\t})\n\t}\n\n\tasync send(\n\t\trequest: WorkerRequest,\n\t\t_clientId?: string,\n\t\toptions: WorkerSendOptions = {},\n\t): Promise<WorkerResponse> {\n\t\tif (this.terminated) {\n\t\t\treturn {\n\t\t\t\tid: request.id,\n\t\t\t\ttype: 'error',\n\t\t\t\tmessage: 'Follower bridge terminated',\n\t\t\t\tcode: 'BRIDGE_TERMINATED',\n\t\t\t}\n\t\t}\n\n\t\tconst requestId = request.requestId ?? createClientId()\n\t\tconst signal = options.signal\n\t\tif (signal?.aborted) {\n\t\t\tthrow new RequestAbortedError(request.type, requestId)\n\t\t}\n\t\tconst msg: RpcRequestMessage = {\n\t\t\ttype: RPC_REQUEST,\n\t\t\trequestId,\n\t\t\tclientId: this.clientId,\n\t\t\trequest: { ...request, requestId },\n\t\t}\n\t\tconst timeoutMs = options.timeoutMs ?? this.timeoutMs\n\n\t\treturn new Promise<WorkerResponse>((resolve, reject) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined\n\t\t\tconst onAbort = (): void => {\n\t\t\t\tsettle(() => reject(new RequestAbortedError(request.type, requestId)))\n\t\t\t}\n\t\t\tconst settle = (fn: () => void): void => {\n\t\t\t\tif (timer !== undefined) clearTimeout(timer)\n\t\t\t\tsignal?.removeEventListener('abort', onAbort)\n\t\t\t\tthis.pending.delete(requestId)\n\t\t\t\tthis.stopWatchdogIfIdle()\n\t\t\t\tfn()\n\t\t\t}\n\t\t\tif (Number.isFinite(timeoutMs)) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tsettle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, timeoutMs)))\n\t\t\t\t}, timeoutMs)\n\t\t\t}\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\n\t\t\tthis.pending.set(requestId, {\n\t\t\t\ttype: request.type,\n\t\t\t\tresolve: (response) => settle(() => resolve(response)),\n\t\t\t\treject: (error) => settle(() => reject(error)),\n\t\t\t})\n\t\t\tthis.startWatchdog()\n\t\t\tthis.channel.postMessage(msg)\n\t\t})\n\t}\n\n\tterminate(): void {\n\t\tif (this.terminated) {\n\t\t\treturn\n\t\t}\n\t\tthis.terminated = true\n\t\tthis.leaveLeader()\n\t\tif (typeof removeEventListener === 'function') {\n\t\t\tremoveEventListener('pagehide', this.onPageHide)\n\t\t}\n\t\tthis.channel.close()\n\t\tfor (const [, entry] of [...this.pending]) {\n\t\t\tentry.reject(new BridgeTerminatedError(entry.type, 'follower bridge terminated'))\n\t\t}\n\t\tthis.pending.clear()\n\t\tthis.stopWatchdog()\n\t}\n\n\tprivate markHeard(): void {\n\t\tthis.lastHeardAt = Date.now()\n\t\tthis.heardSinceWatch = true\n\t}\n\n\tprivate startWatchdog(): void {\n\t\tif (this.watchdog !== undefined) return\n\t\t// A recent beat means a leader is known to exist; otherwise the silence\n\t\t// clock starts now.\n\t\tconst now = Date.now()\n\t\tthis.heardSinceWatch = now - this.lastHeardAt < this.heartbeatMs * 2\n\t\tif (!this.heardSinceWatch) this.lastHeardAt = now\n\t\tthis.watchdog = setInterval(() => this.checkLiveness(), this.heartbeatMs)\n\t}\n\n\tprivate stopWatchdogIfIdle(): void {\n\t\tif (this.pending.size === 0) this.stopWatchdog()\n\t}\n\n\tprivate stopWatchdog(): void {\n\t\tif (this.watchdog !== undefined) {\n\t\t\tclearInterval(this.watchdog)\n\t\t\tthis.watchdog = undefined\n\t\t}\n\t}\n\n\tprivate checkLiveness(): void {\n\t\tif (this.pending.size === 0) {\n\t\t\tthis.stopWatchdog()\n\t\t\treturn\n\t\t}\n\t\tconst silentMs = Date.now() - this.lastHeardAt\n\t\tconst heard = this.heardSinceWatch\n\t\tconst limit = this.heartbeatMs * (heard ? MISSED_BEATS_UNRESPONSIVE : MISSED_BEATS_ABSENT)\n\t\tif (silentMs >= limit) {\n\t\t\tfor (const [requestId, entry] of [...this.pending]) {\n\t\t\t\tentry.reject(\n\t\t\t\t\theard\n\t\t\t\t\t\t? new LeaderUnresponsiveError(`follower-rpc:${entry.type}`, silentMs, requestId)\n\t\t\t\t\t\t: new NoLeaderError(`follower-rpc:${entry.type}`),\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif (silentMs >= this.heartbeatMs) {\n\t\t\t// Pull a sign of life from leaders that do not push heartbeats.\n\t\t\tthis.channel.postMessage({ type: LEADER_PING })\n\t\t}\n\t}\n\n\tprivate leaveLeader(): void {\n\t\ttry {\n\t\t\tconst message: ClientLeaveMessage = {\n\t\t\t\ttype: CLIENT_LEAVE,\n\t\t\t\tclientId: this.clientId,\n\t\t\t}\n\t\t\tthis.channel.postMessage(message)\n\t\t} catch {\n\t\t\t// Best effort: the serializer's idle rollback is the guaranteed backstop.\n\t\t}\n\t}\n}\n\ninterface QueuedWorkerRequest {\n\tclientId: string\n\trequest: WorkerRequest\n\tresolve: (response: WorkerResponse) => void\n\treject: (error: Error) => void\n}\n\n/**\n * Serializes one SQLite worker across the leader tab and all follower tabs.\n *\n * SQLite transactions are represented as multiple worker messages\n * (`begin`, one or more reads/writes, then `commit`/`rollback`). Per-tab mutexes\n * cannot protect that span because follower messages converge at the leader.\n * This bridge promotes the worker boundary into the serialization point, so no\n * other client can interleave while a client owns an active transaction.\n */\nexport class TransactionSerializingWorkerBridge implements WorkerBridge {\n\tprivate readonly inner: WorkerBridge\n\tprivate readonly leaderClientId = createClientId()\n\tprivate readonly transactionIdleTimeoutMs: number\n\tprivate queue: QueuedWorkerRequest[] = []\n\tprivate activeTransactionClient: string | null = null\n\tprivate abortedClients = new Set<string>()\n\tprivate transactionIdleTimer: ReturnType<typeof setTimeout> | null = null\n\tprivate reclaiming = false\n\tprivate processing = false\n\tprivate terminated = false\n\n\tconstructor(inner: WorkerBridge, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {\n\t\tthis.inner = inner\n\t\tthis.transactionIdleTimeoutMs = transactionIdleTimeoutMs\n\t}\n\n\tsend(request: WorkerRequest, clientId = this.leaderClientId): Promise<WorkerResponse> {\n\t\tif (this.terminated) {\n\t\t\treturn Promise.resolve({\n\t\t\t\tid: request.id,\n\t\t\t\ttype: 'error',\n\t\t\t\tmessage: 'Worker has been terminated',\n\t\t\t\tcode: 'WORKER_TERMINATED',\n\t\t\t})\n\t\t}\n\t\tif (this.abortedClients.has(clientId)) {\n\t\t\tif (request.type === 'begin') {\n\t\t\t\tthis.abortedClients.delete(clientId)\n\t\t\t} else {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\tid: request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: 'Previous transaction was aborted because the client stopped sending requests.',\n\t\t\t\t\tcode: 'TRANSACTION_ABORTED',\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\treturn new Promise<WorkerResponse>((resolve, reject) => {\n\t\t\tthis.queue.push({ clientId, request, resolve, reject })\n\t\t\tvoid this.processQueue()\n\t\t})\n\t}\n\n\tterminate(): void {\n\t\tif (this.terminated) {\n\t\t\treturn\n\t\t}\n\t\tthis.terminated = true\n\t\tthis.clearTransactionIdleTimer()\n\t\tthis.inner.terminate()\n\t\tconst pending = this.queue.splice(0)\n\t\tfor (const entry of pending) {\n\t\t\tentry.reject(new BridgeTerminatedError(entry.request.type, 'worker terminated'))\n\t\t}\n\t}\n\n\treclaimClient(clientId: string, reason: string): void {\n\t\tif (this.terminated) {\n\t\t\treturn\n\t\t}\n\t\tvoid this.reclaimClientNow(clientId, reason)\n\t}\n\n\tprivate async processQueue(): Promise<void> {\n\t\tif (this.processing || this.reclaiming) {\n\t\t\treturn\n\t\t}\n\t\tthis.processing = true\n\t\ttry {\n\t\t\twhile (!this.terminated) {\n\t\t\t\tconst index = this.nextRunnableIndex()\n\t\t\t\tif (index === -1) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst [entry] = this.queue.splice(index, 1)\n\t\t\t\tif (!entry) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await this.inner.send(entry.request)\n\t\t\t\t\tthis.recordTransactionState(entry, response)\n\t\t\t\t\tentry.resolve(response)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.resetFailedTransaction(entry)\n\t\t\t\t\tentry.reject(error instanceof Error ? error : new Error(String(error)))\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.processing = false\n\t\t\tif (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {\n\t\t\t\tvoid this.processQueue()\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate nextRunnableIndex(): number {\n\t\tif (this.queue.length === 0) {\n\t\t\treturn -1\n\t\t}\n\t\tif (this.activeTransactionClient === null) {\n\t\t\treturn 0\n\t\t}\n\t\treturn this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient)\n\t}\n\n\tprivate recordTransactionState(entry: QueuedWorkerRequest, response: WorkerResponse): void {\n\t\tif (response.type === 'error') {\n\t\t\tthis.resetFailedTransaction(entry)\n\t\t\treturn\n\t\t}\n\t\tif (entry.request.type === 'begin') {\n\t\t\tthis.activeTransactionClient = entry.clientId\n\t\t\tthis.armTransactionIdleTimer(entry.clientId)\n\t\t\treturn\n\t\t}\n\t\tif (this.activeTransactionClient === entry.clientId) {\n\t\t\tthis.armTransactionIdleTimer(entry.clientId)\n\t\t}\n\t\tif (\n\t\t\tthis.activeTransactionClient === entry.clientId &&\n\t\t\t(entry.request.type === 'commit' || entry.request.type === 'rollback')\n\t\t) {\n\t\t\tthis.activeTransactionClient = null\n\t\t\tthis.clearTransactionIdleTimer()\n\t\t}\n\t}\n\n\tprivate resetFailedTransaction(entry: QueuedWorkerRequest): void {\n\t\tif (this.activeTransactionClient === entry.clientId) {\n\t\t\tthis.activeTransactionClient = null\n\t\t\tthis.clearTransactionIdleTimer()\n\t\t}\n\t}\n\n\tprivate armTransactionIdleTimer(clientId: string): void {\n\t\tthis.clearTransactionIdleTimer()\n\t\tthis.transactionIdleTimer = setTimeout(() => {\n\t\t\tvoid this.reclaimClientNow(clientId, 'transaction-idle-timeout')\n\t\t}, this.transactionIdleTimeoutMs)\n\t}\n\n\tprivate clearTransactionIdleTimer(): void {\n\t\tif (this.transactionIdleTimer) {\n\t\t\tclearTimeout(this.transactionIdleTimer)\n\t\t\tthis.transactionIdleTimer = null\n\t\t}\n\t}\n\n\tprivate async reclaimClientNow(clientId: string, reason: string): Promise<void> {\n\t\tif (this.reclaiming || this.activeTransactionClient !== clientId) {\n\t\t\treturn\n\t\t}\n\t\tthis.reclaiming = true\n\t\tthis.clearTransactionIdleTimer()\n\t\tthis.activeTransactionClient = null\n\t\tthis.abortedClients.add(clientId)\n\t\tthis.rejectQueuedClientRequests(clientId, reason)\n\n\t\ttry {\n\t\t\tconst response = await this.inner.send({ id: 0, type: 'rollback' })\n\t\t\tif (response.type === 'error') {\n\t\t\t\t// If there was no active SQLite transaction left, the important state\n\t\t\t\t// is already reclaimed in JS. The next request will establish a fresh\n\t\t\t\t// transaction if needed.\n\t\t\t}\n\t\t} catch {\n\t\t\t// Keep the queue moving. The inner worker will surface any unrecoverable\n\t\t\t// state through subsequent requests.\n\t\t} finally {\n\t\t\tthis.reclaiming = false\n\t\t\tif (!this.terminated && this.nextRunnableIndex() !== -1) {\n\t\t\t\tvoid this.processQueue()\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate rejectQueuedClientRequests(clientId: string, reason: string): void {\n\t\tconst keep: QueuedWorkerRequest[] = []\n\t\tfor (const entry of this.queue) {\n\t\t\tif (entry.clientId === clientId) {\n\t\t\t\tentry.resolve({\n\t\t\t\t\tid: entry.request.id,\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\tmessage: `Transaction client was reclaimed: ${reason}`,\n\t\t\t\t\tcode: 'TRANSACTION_ABORTED',\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tkeep.push(entry)\n\t\t\t}\n\t\t}\n\t\tthis.queue = keep\n\t}\n}\n\nfunction hasReclaimClient(bridge: WorkerBridge): bridge is ReclaimingWorkerBridge {\n\treturn typeof (bridge as Partial<ReclaimingWorkerBridge>).reclaimClient === 'function'\n}\n\nfunction createClientId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\treturn `${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n"],"mappings":"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","names":[]}
|