@korajs/store 1.0.0-beta.2 → 1.0.0-beta.4
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/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/indexeddb.cjs +157 -34
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.js +3 -3
- package/dist/adapters/sqlite-wasm-shared-host.cjs +80 -53
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +63 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +63 -1
- package/dist/adapters/sqlite-wasm-shared-host.js +56 -52
- package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +157 -34
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +13 -2
- package/dist/adapters/sqlite-wasm.d.ts +13 -2
- package/dist/adapters/sqlite-wasm.js +3 -3
- package/dist/{chunk-QUJGXSZ6.js → chunk-2ZRQGFEF.js} +150 -36
- package/dist/chunk-2ZRQGFEF.js.map +1 -0
- package/dist/{chunk-IOEFNHHP.js → chunk-COJSQBXW.js} +2 -2
- package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
- package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
- package/dist/{chunk-2VRPF25R.js → chunk-UXRWVXBP.js} +2 -2
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-CNHRf-m2.d.cts} +1 -1
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-CNHRf-m2.d.ts} +1 -1
- package/dist/sqlite-wasm-channel-NGSBJR44.js +12 -0
- package/package.json +2 -2
- package/dist/chunk-QUJGXSZ6.js.map +0 -1
- package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
- /package/dist/{chunk-IOEFNHHP.js.map → chunk-COJSQBXW.js.map} +0 -0
- /package/dist/{chunk-2VRPF25R.js.map → chunk-UXRWVXBP.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-NGSBJR44.js.map} +0 -0
|
@@ -1,2 +1,64 @@
|
|
|
1
|
+
import { a as WorkerResponse, b as WorkerRequest } from '../sqlite-wasm-channel-CNHRf-m2.cjs';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* SharedWorker host: one DedicatedWorker (SQLite WASM) per `dbName` per browser origin.
|
|
5
|
+
*
|
|
6
|
+
* Bundle this file separately and pass its URL as `store.sharedWorkerUrl` alongside `workerUrl`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* createApp({
|
|
11
|
+
* store: {
|
|
12
|
+
* workerUrl: '/sqlite-wasm-worker.js',
|
|
13
|
+
* sharedWorkerUrl: '/sqlite-wasm-shared-host.js',
|
|
14
|
+
* },
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
declare const SW_RESPONSE = "kora-sw-response";
|
|
20
|
+
interface SharedWorkerRpcResponse {
|
|
21
|
+
type: typeof SW_RESPONSE;
|
|
22
|
+
requestId: string;
|
|
23
|
+
response: WorkerResponse;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Minimal structural view of the inner SQLite DedicatedWorker the host owns.
|
|
27
|
+
* Declared here (rather than using the DOM `Worker` type directly) so the host
|
|
28
|
+
* routing can be unit-tested with a mock worker.
|
|
29
|
+
*/
|
|
30
|
+
interface HostWorker {
|
|
31
|
+
postMessage(message: WorkerRequest): void;
|
|
32
|
+
onmessage: ((event: {
|
|
33
|
+
data: WorkerResponse;
|
|
34
|
+
}) => void) | null;
|
|
35
|
+
terminate?(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Minimal structural view of a connected client `MessagePort`. */
|
|
38
|
+
interface HostPort {
|
|
39
|
+
start(): void;
|
|
40
|
+
postMessage(message: SharedWorkerRpcResponse): void;
|
|
41
|
+
addEventListener(type: 'message' | 'messageerror', handler: (event: {
|
|
42
|
+
data: unknown;
|
|
43
|
+
}) => void): void;
|
|
44
|
+
}
|
|
45
|
+
interface SharedWorkerHostOptions {
|
|
46
|
+
/** Spawns the inner SQLite worker for a database. Injectable for tests. */
|
|
47
|
+
spawnWorker: (workerUrl: string) => HostWorker;
|
|
48
|
+
}
|
|
49
|
+
interface SharedWorkerHost {
|
|
50
|
+
/** Wire a newly connected client port into the host. */
|
|
51
|
+
connect(port: HostPort): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates the SharedWorker host routing core: one inner SQLite worker per
|
|
55
|
+
* `dbName`, fanning requests from every connected tab through it and routing each
|
|
56
|
+
* response back to the exact requester.
|
|
57
|
+
*
|
|
58
|
+
* Correlation is keyed on a host-minted inner id, never on the incoming
|
|
59
|
+
* `request.id` (which is `0` for every request from every tab). This is what keeps
|
|
60
|
+
* concurrent and multi-tab requests from resolving each other's responses.
|
|
61
|
+
*/
|
|
62
|
+
declare function createSharedWorkerHost(options: SharedWorkerHostOptions): SharedWorkerHost;
|
|
63
|
+
|
|
64
|
+
export { type HostPort, type HostWorker, type SharedWorkerHost, type SharedWorkerHostOptions, createSharedWorkerHost };
|
|
@@ -1,2 +1,64 @@
|
|
|
1
|
+
import { a as WorkerResponse, b as WorkerRequest } from '../sqlite-wasm-channel-CNHRf-m2.js';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* SharedWorker host: one DedicatedWorker (SQLite WASM) per `dbName` per browser origin.
|
|
5
|
+
*
|
|
6
|
+
* Bundle this file separately and pass its URL as `store.sharedWorkerUrl` alongside `workerUrl`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* createApp({
|
|
11
|
+
* store: {
|
|
12
|
+
* workerUrl: '/sqlite-wasm-worker.js',
|
|
13
|
+
* sharedWorkerUrl: '/sqlite-wasm-shared-host.js',
|
|
14
|
+
* },
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
declare const SW_RESPONSE = "kora-sw-response";
|
|
20
|
+
interface SharedWorkerRpcResponse {
|
|
21
|
+
type: typeof SW_RESPONSE;
|
|
22
|
+
requestId: string;
|
|
23
|
+
response: WorkerResponse;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Minimal structural view of the inner SQLite DedicatedWorker the host owns.
|
|
27
|
+
* Declared here (rather than using the DOM `Worker` type directly) so the host
|
|
28
|
+
* routing can be unit-tested with a mock worker.
|
|
29
|
+
*/
|
|
30
|
+
interface HostWorker {
|
|
31
|
+
postMessage(message: WorkerRequest): void;
|
|
32
|
+
onmessage: ((event: {
|
|
33
|
+
data: WorkerResponse;
|
|
34
|
+
}) => void) | null;
|
|
35
|
+
terminate?(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Minimal structural view of a connected client `MessagePort`. */
|
|
38
|
+
interface HostPort {
|
|
39
|
+
start(): void;
|
|
40
|
+
postMessage(message: SharedWorkerRpcResponse): void;
|
|
41
|
+
addEventListener(type: 'message' | 'messageerror', handler: (event: {
|
|
42
|
+
data: unknown;
|
|
43
|
+
}) => void): void;
|
|
44
|
+
}
|
|
45
|
+
interface SharedWorkerHostOptions {
|
|
46
|
+
/** Spawns the inner SQLite worker for a database. Injectable for tests. */
|
|
47
|
+
spawnWorker: (workerUrl: string) => HostWorker;
|
|
48
|
+
}
|
|
49
|
+
interface SharedWorkerHost {
|
|
50
|
+
/** Wire a newly connected client port into the host. */
|
|
51
|
+
connect(port: HostPort): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates the SharedWorker host routing core: one inner SQLite worker per
|
|
55
|
+
* `dbName`, fanning requests from every connected tab through it and routing each
|
|
56
|
+
* response back to the exact requester.
|
|
57
|
+
*
|
|
58
|
+
* Correlation is keyed on a host-minted inner id, never on the incoming
|
|
59
|
+
* `request.id` (which is `0` for every request from every tab). This is what keeps
|
|
60
|
+
* concurrent and multi-tab requests from resolving each other's responses.
|
|
61
|
+
*/
|
|
62
|
+
declare function createSharedWorkerHost(options: SharedWorkerHostOptions): SharedWorkerHost;
|
|
63
|
+
|
|
64
|
+
export { type HostPort, type HostWorker, type SharedWorkerHost, type SharedWorkerHostOptions, createSharedWorkerHost };
|
|
@@ -1,66 +1,70 @@
|
|
|
1
1
|
// src/adapters/sqlite-wasm-shared-host.ts
|
|
2
2
|
var SW_REQUEST = "kora-sw-request";
|
|
3
3
|
var SW_RESPONSE = "kora-sw-response";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const worker = new Worker(workerUrl, { type: "module" });
|
|
11
|
-
const entry = { worker, pending: /* @__PURE__ */ new Map() };
|
|
12
|
-
worker.onmessage = (event) => {
|
|
13
|
-
const response = event.data;
|
|
14
|
-
const requestId = entry.pending.get(response.id);
|
|
15
|
-
if (!requestId) {
|
|
16
|
-
return;
|
|
4
|
+
function createSharedWorkerHost(options) {
|
|
5
|
+
const pools = /* @__PURE__ */ new Map();
|
|
6
|
+
function getPool(dbName, workerUrl) {
|
|
7
|
+
const existing = pools.get(dbName);
|
|
8
|
+
if (existing) {
|
|
9
|
+
return existing;
|
|
17
10
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
11
|
+
const worker = options.spawnWorker(workerUrl);
|
|
12
|
+
const entry = { worker, nextInnerId: 1, pending: /* @__PURE__ */ new Map() };
|
|
13
|
+
worker.onmessage = (event) => {
|
|
14
|
+
const response = event.data;
|
|
15
|
+
const waiting = entry.pending.get(response.id);
|
|
16
|
+
if (!waiting) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
entry.pending.delete(response.id);
|
|
21
20
|
const msg = {
|
|
22
21
|
type: SW_RESPONSE,
|
|
23
|
-
requestId,
|
|
22
|
+
requestId: waiting.requestId,
|
|
24
23
|
response
|
|
25
24
|
};
|
|
26
|
-
port.postMessage(msg);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return entry;
|
|
31
|
-
}
|
|
32
|
-
var portRequestIds = /* @__PURE__ */ new Map();
|
|
33
|
-
var requestPort = /* @__PURE__ */ new Map();
|
|
34
|
-
function findPortForRequest(requestId) {
|
|
35
|
-
return requestPort.get(requestId);
|
|
36
|
-
}
|
|
37
|
-
self.onconnect = (event) => {
|
|
38
|
-
const port = event.ports[0];
|
|
39
|
-
if (!port) {
|
|
40
|
-
return;
|
|
25
|
+
waiting.port.postMessage(msg);
|
|
26
|
+
};
|
|
27
|
+
pools.set(dbName, entry);
|
|
28
|
+
return entry;
|
|
41
29
|
}
|
|
42
|
-
port
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
function connect(port) {
|
|
31
|
+
port.start();
|
|
32
|
+
port.addEventListener("message", (event) => {
|
|
33
|
+
const data = event.data;
|
|
34
|
+
if (data?.type !== SW_REQUEST) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const entry = getPool(data.dbName, data.workerUrl);
|
|
38
|
+
const innerId = entry.nextInnerId;
|
|
39
|
+
entry.nextInnerId += 1;
|
|
40
|
+
entry.pending.set(innerId, { requestId: data.requestId, port });
|
|
41
|
+
entry.worker.postMessage({ ...data.request, id: innerId });
|
|
42
|
+
});
|
|
43
|
+
port.addEventListener("messageerror", () => {
|
|
44
|
+
for (const entry of pools.values()) {
|
|
45
|
+
for (const [innerId, waiting] of entry.pending) {
|
|
46
|
+
if (waiting.port === port) {
|
|
47
|
+
entry.pending.delete(innerId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { connect };
|
|
54
|
+
}
|
|
55
|
+
if (typeof self !== "undefined" && "onconnect" in self) {
|
|
56
|
+
const host = createSharedWorkerHost({
|
|
57
|
+
spawnWorker: (workerUrl) => new Worker(workerUrl, { type: "module" })
|
|
54
58
|
});
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
59
|
+
self.onconnect = (event) => {
|
|
60
|
+
const port = event.ports[0];
|
|
61
|
+
if (!port) {
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
host.connect(port);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
createSharedWorkerHost
|
|
65
69
|
};
|
|
66
70
|
//# sourceMappingURL=sqlite-wasm-shared-host.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/sqlite-wasm-shared-host.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * SharedWorker host: one DedicatedWorker (SQLite WASM) per `dbName` per browser origin.\n *\n * Bundle this file separately and pass its URL as `store.sharedWorkerUrl` alongside `workerUrl`.\n *\n * @example\n * ```typescript\n * createApp({\n * store: {\n * workerUrl: '/sqlite-wasm-worker.js',\n * sharedWorkerUrl: '/sqlite-wasm-shared-host.js',\n * },\n * })\n * ```\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\nconst SW_REQUEST = 'kora-sw-request'\nconst SW_RESPONSE = 'kora-sw-response'\n\ninterface SharedWorkerRpcRequest {\n\ttype: typeof SW_REQUEST\n\trequestId: string\n\tdbName: string\n\tworkerUrl: string\n\trequest: WorkerRequest\n}\n\ninterface SharedWorkerRpcResponse {\n\ttype: typeof SW_RESPONSE\n\trequestId: string\n\tresponse: WorkerResponse\n}\n\ninterface WorkerEntry {\n\tworker:
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/sqlite-wasm-shared-host.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n/**\n * SharedWorker host: one DedicatedWorker (SQLite WASM) per `dbName` per browser origin.\n *\n * Bundle this file separately and pass its URL as `store.sharedWorkerUrl` alongside `workerUrl`.\n *\n * @example\n * ```typescript\n * createApp({\n * store: {\n * workerUrl: '/sqlite-wasm-worker.js',\n * sharedWorkerUrl: '/sqlite-wasm-shared-host.js',\n * },\n * })\n * ```\n */\n\nimport type { WorkerRequest, WorkerResponse } from './sqlite-wasm-channel'\n\nconst SW_REQUEST = 'kora-sw-request'\nconst SW_RESPONSE = 'kora-sw-response'\n\ninterface SharedWorkerRpcRequest {\n\ttype: typeof SW_REQUEST\n\trequestId: string\n\tdbName: string\n\tworkerUrl: string\n\trequest: WorkerRequest\n}\n\ninterface SharedWorkerRpcResponse {\n\ttype: typeof SW_RESPONSE\n\trequestId: string\n\tresponse: WorkerResponse\n}\n\n/**\n * Minimal structural view of the inner SQLite DedicatedWorker the host owns.\n * Declared here (rather than using the DOM `Worker` type directly) so the host\n * routing can be unit-tested with a mock worker.\n */\nexport interface HostWorker {\n\tpostMessage(message: WorkerRequest): void\n\tonmessage: ((event: { data: WorkerResponse }) => void) | null\n\tterminate?(): void\n}\n\n/** Minimal structural view of a connected client `MessagePort`. */\nexport interface HostPort {\n\tstart(): void\n\tpostMessage(message: SharedWorkerRpcResponse): void\n\taddEventListener(\n\t\ttype: 'message' | 'messageerror',\n\t\thandler: (event: { data: unknown }) => void,\n\t): void\n}\n\nexport interface SharedWorkerHostOptions {\n\t/** Spawns the inner SQLite worker for a database. Injectable for tests. */\n\tspawnWorker: (workerUrl: string) => HostWorker\n}\n\ninterface WorkerEntry {\n\tworker: HostWorker\n\t/**\n\t * Monotonic id assigned to each request FORWARDED to the inner worker. The\n\t * adapter always sends `request.id === 0` (uniqueness is a bridge concern), and\n\t * every tab shares one inner worker, so the host must mint its own unique id per\n\t * request or concurrent requests collide on the correlation key. This mirrors\n\t * `WebWorkerBridge`, which reassigns `id` before posting to its worker.\n\t */\n\tnextInnerId: number\n\t/** innerId -> the client waiting for that response. */\n\tpending: Map<number, { requestId: string; port: HostPort }>\n}\n\nexport interface SharedWorkerHost {\n\t/** Wire a newly connected client port into the host. */\n\tconnect(port: HostPort): void\n}\n\n/**\n * Creates the SharedWorker host routing core: one inner SQLite worker per\n * `dbName`, fanning requests from every connected tab through it and routing each\n * response back to the exact requester.\n *\n * Correlation is keyed on a host-minted inner id, never on the incoming\n * `request.id` (which is `0` for every request from every tab). This is what keeps\n * concurrent and multi-tab requests from resolving each other's responses.\n */\nexport function createSharedWorkerHost(options: SharedWorkerHostOptions): SharedWorkerHost {\n\tconst pools = new Map<string, WorkerEntry>()\n\n\tfunction getPool(dbName: string, workerUrl: string): WorkerEntry {\n\t\tconst existing = pools.get(dbName)\n\t\tif (existing) {\n\t\t\treturn existing\n\t\t}\n\n\t\tconst worker = options.spawnWorker(workerUrl)\n\t\tconst entry: WorkerEntry = { worker, nextInnerId: 1, pending: new Map() }\n\n\t\tworker.onmessage = (event: { data: WorkerResponse }): void => {\n\t\t\tconst response = event.data\n\t\t\tconst waiting = entry.pending.get(response.id)\n\t\t\tif (!waiting) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tentry.pending.delete(response.id)\n\t\t\tconst msg: SharedWorkerRpcResponse = {\n\t\t\t\ttype: SW_RESPONSE,\n\t\t\t\trequestId: waiting.requestId,\n\t\t\t\tresponse,\n\t\t\t}\n\t\t\twaiting.port.postMessage(msg)\n\t\t}\n\n\t\tpools.set(dbName, entry)\n\t\treturn entry\n\t}\n\n\tfunction connect(port: HostPort): void {\n\t\tport.start()\n\n\t\tport.addEventListener('message', (event: { data: unknown }): void => {\n\t\t\tconst data = event.data as SharedWorkerRpcRequest | undefined\n\t\t\tif (data?.type !== SW_REQUEST) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst entry = getPool(data.dbName, data.workerUrl)\n\t\t\tconst innerId = entry.nextInnerId\n\t\t\tentry.nextInnerId += 1\n\t\t\tentry.pending.set(innerId, { requestId: data.requestId, port })\n\t\t\t// Reassign a unique inner id. The client bridge correlates on the outer\n\t\t\t// requestId, so rewriting request.id here is transparent to the caller.\n\t\t\tentry.worker.postMessage({ ...data.request, id: innerId })\n\t\t})\n\n\t\tport.addEventListener('messageerror', (): void => {\n\t\t\t// Best-effort cleanup: drop any in-flight requests routed to this port so a\n\t\t\t// disconnected tab does not leak pending entries.\n\t\t\tfor (const entry of pools.values()) {\n\t\t\t\tfor (const [innerId, waiting] of entry.pending) {\n\t\t\t\t\tif (waiting.port === port) {\n\t\t\t\t\t\tentry.pending.delete(innerId)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\treturn { connect }\n}\n\n// Wire the host to the SharedWorker global only when running as an actual\n// SharedWorker. Guarding this keeps the module importable in Node test runtimes,\n// where `createSharedWorkerHost` is unit-tested directly with mock ports/workers.\ndeclare const self: SharedWorkerGlobalScope\nif (typeof self !== 'undefined' && 'onconnect' in self) {\n\tconst host = createSharedWorkerHost({\n\t\tspawnWorker: (workerUrl: string): HostWorker =>\n\t\t\tnew Worker(workerUrl, { type: 'module' }) as unknown as HostWorker,\n\t})\n\n\tself.onconnect = (event: MessageEvent): void => {\n\t\tconst port = event.ports[0]\n\t\tif (!port) {\n\t\t\treturn\n\t\t}\n\t\thost.connect(port as unknown as HostPort)\n\t}\n}\n"],"mappings":";AAmBA,IAAM,aAAa;AACnB,IAAM,cAAc;AAsEb,SAAS,uBAAuB,SAAoD;AAC1F,QAAM,QAAQ,oBAAI,IAAyB;AAE3C,WAAS,QAAQ,QAAgB,WAAgC;AAChE,UAAM,WAAW,MAAM,IAAI,MAAM;AACjC,QAAI,UAAU;AACb,aAAO;AAAA,IACR;AAEA,UAAM,SAAS,QAAQ,YAAY,SAAS;AAC5C,UAAM,QAAqB,EAAE,QAAQ,aAAa,GAAG,SAAS,oBAAI,IAAI,EAAE;AAExE,WAAO,YAAY,CAAC,UAA0C;AAC7D,YAAM,WAAW,MAAM;AACvB,YAAM,UAAU,MAAM,QAAQ,IAAI,SAAS,EAAE;AAC7C,UAAI,CAAC,SAAS;AACb;AAAA,MACD;AACA,YAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,YAAM,MAA+B;AAAA,QACpC,MAAM;AAAA,QACN,WAAW,QAAQ;AAAA,QACnB;AAAA,MACD;AACA,cAAQ,KAAK,YAAY,GAAG;AAAA,IAC7B;AAEA,UAAM,IAAI,QAAQ,KAAK;AACvB,WAAO;AAAA,EACR;AAEA,WAAS,QAAQ,MAAsB;AACtC,SAAK,MAAM;AAEX,SAAK,iBAAiB,WAAW,CAAC,UAAmC;AACpE,YAAM,OAAO,MAAM;AACnB,UAAI,MAAM,SAAS,YAAY;AAC9B;AAAA,MACD;AAEA,YAAM,QAAQ,QAAQ,KAAK,QAAQ,KAAK,SAAS;AACjD,YAAM,UAAU,MAAM;AACtB,YAAM,eAAe;AACrB,YAAM,QAAQ,IAAI,SAAS,EAAE,WAAW,KAAK,WAAW,KAAK,CAAC;AAG9D,YAAM,OAAO,YAAY,EAAE,GAAG,KAAK,SAAS,IAAI,QAAQ,CAAC;AAAA,IAC1D,CAAC;AAED,SAAK,iBAAiB,gBAAgB,MAAY;AAGjD,iBAAW,SAAS,MAAM,OAAO,GAAG;AACnC,mBAAW,CAAC,SAAS,OAAO,KAAK,MAAM,SAAS;AAC/C,cAAI,QAAQ,SAAS,MAAM;AAC1B,kBAAM,QAAQ,OAAO,OAAO;AAAA,UAC7B;AAAA,QACD;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ;AAClB;AAMA,IAAI,OAAO,SAAS,eAAe,eAAe,MAAM;AACvD,QAAM,OAAO,uBAAuB;AAAA,IACnC,aAAa,CAAC,cACb,IAAI,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC;AAAA,EAC1C,CAAC;AAED,OAAK,YAAY,CAAC,UAA8B;AAC/C,UAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,QAAI,CAAC,MAAM;AACV;AAAA,IACD;AACA,SAAK,QAAQ,IAA2B;AAAA,EACzC;AACD;","names":[]}
|
|
@@ -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;
|
|
24
|
+
var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError;
|
|
25
25
|
var init_errors = __esm({
|
|
26
26
|
"src/errors.ts"() {
|
|
27
27
|
"use strict";
|
|
@@ -48,6 +48,16 @@ var init_errors = __esm({
|
|
|
48
48
|
this.name = "WorkerTimeoutError";
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
NoLeaderError = class extends import_core.KoraError {
|
|
52
|
+
constructor(operation) {
|
|
53
|
+
super(
|
|
54
|
+
`No live leader tab is answering multi-tab storage RPC for operation "${operation}"`,
|
|
55
|
+
"NO_LEADER",
|
|
56
|
+
{ operation }
|
|
57
|
+
);
|
|
58
|
+
this.name = "NoLeaderError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
51
61
|
}
|
|
52
62
|
});
|
|
53
63
|
|
|
@@ -270,45 +280,73 @@ var SharedWorkerClientBridge = class {
|
|
|
270
280
|
init_errors();
|
|
271
281
|
var RPC_REQUEST = "kora-worker-request";
|
|
272
282
|
var RPC_RESPONSE = "kora-worker-response";
|
|
283
|
+
var LEADER_PING = "kora-leader-ping";
|
|
284
|
+
var LEADER_PONG = "kora-leader-pong";
|
|
285
|
+
var DEFAULT_LIVENESS_PROBE_MS = 2e3;
|
|
273
286
|
function isSharedWorkerStorageSupported() {
|
|
274
287
|
return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
|
|
275
288
|
}
|
|
276
289
|
function isMultiTabStorageSupported() {
|
|
277
290
|
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
278
291
|
}
|
|
279
|
-
async function acquireTabStorageSession(dbName) {
|
|
292
|
+
async function acquireTabStorageSession(dbName, options) {
|
|
280
293
|
const channelName = `kora-storage-${dbName}`;
|
|
294
|
+
const lockName = `kora-leader-${dbName}`;
|
|
281
295
|
if (!isMultiTabStorageSupported()) {
|
|
282
296
|
return { role: "leader", channelName };
|
|
283
297
|
}
|
|
284
298
|
return new Promise((resolve) => {
|
|
285
299
|
let releaseHeld;
|
|
286
|
-
void navigator.locks.request(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (lock === null) {
|
|
291
|
-
resolve({ role: "follower", channelName });
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
resolve({
|
|
295
|
-
role: "leader",
|
|
296
|
-
channelName,
|
|
297
|
-
releaseLock: async () => {
|
|
298
|
-
releaseHeld?.();
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
return new Promise((release) => {
|
|
302
|
-
releaseHeld = release;
|
|
303
|
-
});
|
|
300
|
+
void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
|
|
301
|
+
if (lock === null) {
|
|
302
|
+
resolve(startFollowerSession(channelName, lockName, options));
|
|
303
|
+
return;
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
resolve({
|
|
306
|
+
role: "leader",
|
|
307
|
+
channelName,
|
|
308
|
+
releaseLock: async () => {
|
|
309
|
+
releaseHeld?.();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return new Promise((release) => {
|
|
313
|
+
releaseHeld = release;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
306
316
|
});
|
|
307
317
|
}
|
|
318
|
+
function startFollowerSession(channelName, lockName, options) {
|
|
319
|
+
let promoted = false;
|
|
320
|
+
let releasePromotedLock;
|
|
321
|
+
const abort = new AbortController();
|
|
322
|
+
void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
|
|
323
|
+
promoted = true;
|
|
324
|
+
options?.onPromote?.();
|
|
325
|
+
return new Promise((release) => {
|
|
326
|
+
releasePromotedLock = release;
|
|
327
|
+
});
|
|
328
|
+
}).catch(() => {
|
|
329
|
+
});
|
|
330
|
+
return {
|
|
331
|
+
role: "follower",
|
|
332
|
+
channelName,
|
|
333
|
+
cancelPromotionWatch: () => {
|
|
334
|
+
if (promoted) {
|
|
335
|
+
releasePromotedLock?.();
|
|
336
|
+
} else {
|
|
337
|
+
abort.abort();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
308
342
|
function startLeaderRpcRelay(channelName, bridge) {
|
|
309
343
|
const channel = new BroadcastChannel(channelName);
|
|
310
344
|
const onMessage = (event) => {
|
|
311
345
|
const data = event.data;
|
|
346
|
+
if (data?.type === LEADER_PING) {
|
|
347
|
+
channel.postMessage({ type: LEADER_PONG });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
312
350
|
if (data?.type !== RPC_REQUEST) {
|
|
313
351
|
return;
|
|
314
352
|
}
|
|
@@ -344,9 +382,11 @@ var FollowerBroadcastBridge = class {
|
|
|
344
382
|
channel;
|
|
345
383
|
pending = /* @__PURE__ */ new Map();
|
|
346
384
|
timeoutMs;
|
|
385
|
+
livenessProbeMs;
|
|
347
386
|
terminated = false;
|
|
348
|
-
constructor(channelName, timeoutMs = 3e4) {
|
|
387
|
+
constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
|
|
349
388
|
this.timeoutMs = timeoutMs;
|
|
389
|
+
this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
|
|
350
390
|
this.channel = new BroadcastChannel(channelName);
|
|
351
391
|
this.channel.addEventListener("message", (event) => {
|
|
352
392
|
const data = event.data;
|
|
@@ -360,6 +400,48 @@ var FollowerBroadcastBridge = class {
|
|
|
360
400
|
}
|
|
361
401
|
});
|
|
362
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
405
|
+
* or `false` if none answers within the budget. The adapter calls this before
|
|
406
|
+
* its first RPC so a follower created before a leader relay is live retries the
|
|
407
|
+
* handshake instead of firing into the void and waiting out the full timeout.
|
|
408
|
+
*/
|
|
409
|
+
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
410
|
+
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
411
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
412
|
+
if (this.terminated) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (await this.pingLeader(perAttempt)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
/** Sends one liveness ping and resolves whether a leader answered in time. */
|
|
422
|
+
pingLeader(timeoutMs) {
|
|
423
|
+
if (this.terminated) {
|
|
424
|
+
return Promise.resolve(false);
|
|
425
|
+
}
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
const onPong = (event) => {
|
|
428
|
+
if (event.data?.type === LEADER_PONG) {
|
|
429
|
+
cleanup();
|
|
430
|
+
resolve(true);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const cleanup = () => {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
this.channel.removeEventListener("message", onPong);
|
|
436
|
+
};
|
|
437
|
+
const timer = setTimeout(() => {
|
|
438
|
+
cleanup();
|
|
439
|
+
resolve(false);
|
|
440
|
+
}, timeoutMs);
|
|
441
|
+
this.channel.addEventListener("message", onPong);
|
|
442
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
443
|
+
});
|
|
444
|
+
}
|
|
363
445
|
async send(request) {
|
|
364
446
|
if (this.terminated) {
|
|
365
447
|
return {
|
|
@@ -372,19 +454,25 @@ var FollowerBroadcastBridge = class {
|
|
|
372
454
|
const requestId = crypto.randomUUID();
|
|
373
455
|
const msg = { type: RPC_REQUEST, requestId, request };
|
|
374
456
|
return new Promise((resolve, reject) => {
|
|
375
|
-
const
|
|
457
|
+
const settle = (fn) => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
clearTimeout(probe);
|
|
376
460
|
this.pending.delete(requestId);
|
|
377
|
-
|
|
461
|
+
fn();
|
|
462
|
+
};
|
|
463
|
+
const timer = setTimeout(() => {
|
|
464
|
+
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
|
|
378
465
|
}, this.timeoutMs);
|
|
466
|
+
const probe = setTimeout(() => {
|
|
467
|
+
void this.pingLeader(this.livenessProbeMs).then((alive) => {
|
|
468
|
+
if (!alive && this.pending.has(requestId)) {
|
|
469
|
+
settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}, this.livenessProbeMs);
|
|
379
473
|
this.pending.set(requestId, {
|
|
380
|
-
resolve: (response) =>
|
|
381
|
-
|
|
382
|
-
resolve(response);
|
|
383
|
-
},
|
|
384
|
-
reject: (error) => {
|
|
385
|
-
clearTimeout(timer);
|
|
386
|
-
reject(error);
|
|
387
|
-
}
|
|
474
|
+
resolve: (response) => settle(() => resolve(response)),
|
|
475
|
+
reject: (error) => settle(() => reject(error))
|
|
388
476
|
});
|
|
389
477
|
this.channel.postMessage(msg);
|
|
390
478
|
});
|
|
@@ -416,6 +504,9 @@ var SqliteWasmAdapter = class {
|
|
|
416
504
|
emitter;
|
|
417
505
|
tabSession = null;
|
|
418
506
|
sharedWorker = null;
|
|
507
|
+
/** Retained so a follower promoted to leader can re-open its own worker. */
|
|
508
|
+
schema = null;
|
|
509
|
+
promoting = false;
|
|
419
510
|
constructor(options = {}) {
|
|
420
511
|
this.injectedBridge = options.bridge;
|
|
421
512
|
this.workerUrl = options.workerUrl;
|
|
@@ -434,22 +525,29 @@ var SqliteWasmAdapter = class {
|
|
|
434
525
|
this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
|
|
435
526
|
this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
|
|
436
527
|
} else if (this.workerUrl) {
|
|
437
|
-
this.
|
|
528
|
+
this.schema = schema;
|
|
529
|
+
this.tabSession = await acquireTabStorageSession(this.dbName, {
|
|
530
|
+
onPromote: () => {
|
|
531
|
+
void this.promoteToLeader();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
438
534
|
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
439
535
|
if (this.tabSession.role === "leader") {
|
|
440
536
|
const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
|
|
441
537
|
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
442
538
|
this.bridge = workerBridge;
|
|
443
539
|
} else {
|
|
444
|
-
|
|
540
|
+
const followerBridge = new FollowerBroadcastBridge(
|
|
445
541
|
this.tabSession.channelName,
|
|
446
542
|
this.workerResponseTimeoutMs
|
|
447
543
|
);
|
|
544
|
+
this.bridge = followerBridge;
|
|
448
545
|
this.emitter?.emit({
|
|
449
546
|
type: "store:db-name-collision",
|
|
450
547
|
dbName: this.dbName,
|
|
451
548
|
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.`
|
|
452
549
|
});
|
|
550
|
+
await followerBridge.waitForLeader();
|
|
453
551
|
}
|
|
454
552
|
} else {
|
|
455
553
|
throw new AdapterError(
|
|
@@ -499,6 +597,7 @@ var SqliteWasmAdapter = class {
|
|
|
499
597
|
await this.sendRequest({ id: 0, type: "close" });
|
|
500
598
|
} finally {
|
|
501
599
|
this.tabSession?.stopRelay?.();
|
|
600
|
+
this.tabSession?.cancelPromotionWatch?.();
|
|
502
601
|
if (this.tabSession?.releaseLock) {
|
|
503
602
|
await this.tabSession.releaseLock();
|
|
504
603
|
}
|
|
@@ -509,6 +608,30 @@ var SqliteWasmAdapter = class {
|
|
|
509
608
|
this.opened = false;
|
|
510
609
|
}
|
|
511
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Promotes a follower to leader after the previous leader released the storage
|
|
613
|
+
* lock (its tab closed or crashed). The lock is only granted once the old leader
|
|
614
|
+
* is gone, so opening our own worker here is safe from the OPFS single-writer
|
|
615
|
+
* rule. In-flight follower RPCs are rejected and left to the caller / reactive
|
|
616
|
+
* layer to retry against the new leader.
|
|
617
|
+
*/
|
|
618
|
+
async promoteToLeader() {
|
|
619
|
+
if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
this.promoting = true;
|
|
623
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
624
|
+
const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
|
|
625
|
+
const previousBridge = this.bridge;
|
|
626
|
+
this.bridge = workerBridge;
|
|
627
|
+
if (this.tabSession) {
|
|
628
|
+
this.tabSession.role = "leader";
|
|
629
|
+
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
630
|
+
}
|
|
631
|
+
previousBridge?.terminate();
|
|
632
|
+
const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
|
|
633
|
+
await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
|
|
634
|
+
}
|
|
512
635
|
async execute(sql, params) {
|
|
513
636
|
this.guardOpen();
|
|
514
637
|
const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
|