@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.
Files changed (41) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.js +1 -1
  3. package/dist/adapters/indexeddb.cjs +157 -34
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +1 -1
  6. package/dist/adapters/indexeddb.d.ts +1 -1
  7. package/dist/adapters/indexeddb.js +3 -3
  8. package/dist/adapters/sqlite-wasm-shared-host.cjs +80 -53
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -1
  10. package/dist/adapters/sqlite-wasm-shared-host.d.cts +63 -1
  11. package/dist/adapters/sqlite-wasm-shared-host.d.ts +63 -1
  12. package/dist/adapters/sqlite-wasm-shared-host.js +56 -52
  13. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm.cjs +157 -34
  15. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm.d.cts +13 -2
  17. package/dist/adapters/sqlite-wasm.d.ts +13 -2
  18. package/dist/adapters/sqlite-wasm.js +3 -3
  19. package/dist/{chunk-QUJGXSZ6.js → chunk-2ZRQGFEF.js} +150 -36
  20. package/dist/chunk-2ZRQGFEF.js.map +1 -0
  21. package/dist/{chunk-IOEFNHHP.js → chunk-COJSQBXW.js} +2 -2
  22. package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
  23. package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
  24. package/dist/{chunk-2VRPF25R.js → chunk-UXRWVXBP.js} +2 -2
  25. package/dist/index.cjs +12 -0
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +10 -1
  28. package/dist/index.d.ts +10 -1
  29. package/dist/index.js +4 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal.cjs.map +1 -1
  32. package/dist/internal.js +2 -2
  33. package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-CNHRf-m2.d.cts} +1 -1
  34. package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-CNHRf-m2.d.ts} +1 -1
  35. package/dist/sqlite-wasm-channel-NGSBJR44.js +12 -0
  36. package/package.json +2 -2
  37. package/dist/chunk-QUJGXSZ6.js.map +0 -1
  38. package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
  39. /package/dist/{chunk-IOEFNHHP.js.map → chunk-COJSQBXW.js.map} +0 -0
  40. /package/dist/{chunk-2VRPF25R.js.map → chunk-UXRWVXBP.js.map} +0 -0
  41. /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
- export { }
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
- export { }
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
- var pools = /* @__PURE__ */ new Map();
5
- function getPool(dbName, workerUrl) {
6
- const existing = pools.get(dbName);
7
- if (existing) {
8
- return existing;
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
- entry.pending.delete(response.id);
19
- const port = findPortForRequest(requestId);
20
- if (port) {
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
- pools.set(dbName, entry);
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.start();
43
- portRequestIds.set(port, /* @__PURE__ */ new Set());
44
- port.addEventListener("message", (messageEvent) => {
45
- const data = messageEvent.data;
46
- if (data?.type !== SW_REQUEST) {
47
- return;
48
- }
49
- const pool = getPool(data.dbName, data.workerUrl);
50
- pool.pending.set(data.request.id, data.requestId);
51
- requestPort.set(data.requestId, port);
52
- portRequestIds.get(port)?.add(data.requestId);
53
- pool.worker.postMessage(data.request);
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
- port.addEventListener("messageerror", () => {
56
- const ids = portRequestIds.get(port);
57
- if (!ids) {
59
+ self.onconnect = (event) => {
60
+ const port = event.ports[0];
61
+ if (!port) {
58
62
  return;
59
63
  }
60
- for (const requestId of ids) {
61
- requestPort.delete(requestId);
62
- }
63
- portRequestIds.delete(port);
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: Worker\n\tpending: Map<number, string>\n}\n\ndeclare const self: SharedWorkerGlobalScope\n\nconst pools = new Map<string, WorkerEntry>()\n\nfunction getPool(dbName: string, workerUrl: string): WorkerEntry {\n\tconst existing = pools.get(dbName)\n\tif (existing) {\n\t\treturn existing\n\t}\n\n\tconst worker = new Worker(workerUrl, { type: 'module' })\n\tconst entry: WorkerEntry = { worker, pending: new Map() }\n\n\tworker.onmessage = (event: MessageEvent<WorkerResponse>) => {\n\t\tconst response = event.data\n\t\tconst requestId = entry.pending.get(response.id)\n\t\tif (!requestId) {\n\t\t\treturn\n\t\t}\n\t\tentry.pending.delete(response.id)\n\t\tconst port = findPortForRequest(requestId)\n\t\tif (port) {\n\t\t\tconst msg: SharedWorkerRpcResponse = {\n\t\t\t\ttype: SW_RESPONSE,\n\t\t\t\trequestId,\n\t\t\t\tresponse,\n\t\t\t}\n\t\t\tport.postMessage(msg)\n\t\t}\n\t}\n\n\tpools.set(dbName, entry)\n\treturn entry\n}\n\nconst portRequestIds = new Map<MessagePort, Set<string>>()\nconst requestPort = new Map<string, MessagePort>()\n\nfunction findPortForRequest(requestId: string): MessagePort | undefined {\n\treturn requestPort.get(requestId)\n}\n\nself.onconnect = (event: MessageEvent): void => {\n\tconst port = event.ports[0]\n\tif (!port) {\n\t\treturn\n\t}\n\n\tport.start()\n\tportRequestIds.set(port, new Set())\n\n\tport.addEventListener('message', (messageEvent: MessageEvent<SharedWorkerRpcRequest>) => {\n\t\tconst data = messageEvent.data\n\t\tif (data?.type !== SW_REQUEST) {\n\t\t\treturn\n\t\t}\n\n\t\tconst pool = getPool(data.dbName, data.workerUrl)\n\t\tpool.pending.set(data.request.id, data.requestId)\n\t\trequestPort.set(data.requestId, port)\n\t\tportRequestIds.get(port)?.add(data.requestId)\n\n\t\tpool.worker.postMessage(data.request)\n\t})\n\n\tport.addEventListener('messageerror', () => {\n\t\tconst ids = portRequestIds.get(port)\n\t\tif (!ids) {\n\t\t\treturn\n\t\t}\n\t\tfor (const requestId of ids) {\n\t\t\trequestPort.delete(requestId)\n\t\t}\n\t\tportRequestIds.delete(port)\n\t})\n}\n"],"mappings":";AAmBA,IAAM,aAAa;AACnB,IAAM,cAAc;AAuBpB,IAAM,QAAQ,oBAAI,IAAyB;AAE3C,SAAS,QAAQ,QAAgB,WAAgC;AAChE,QAAM,WAAW,MAAM,IAAI,MAAM;AACjC,MAAI,UAAU;AACb,WAAO;AAAA,EACR;AAEA,QAAM,SAAS,IAAI,OAAO,WAAW,EAAE,MAAM,SAAS,CAAC;AACvD,QAAM,QAAqB,EAAE,QAAQ,SAAS,oBAAI,IAAI,EAAE;AAExD,SAAO,YAAY,CAAC,UAAwC;AAC3D,UAAM,WAAW,MAAM;AACvB,UAAM,YAAY,MAAM,QAAQ,IAAI,SAAS,EAAE;AAC/C,QAAI,CAAC,WAAW;AACf;AAAA,IACD;AACA,UAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,UAAM,OAAO,mBAAmB,SAAS;AACzC,QAAI,MAAM;AACT,YAAM,MAA+B;AAAA,QACpC,MAAM;AAAA,QACN;AAAA,QACA;AAAA,MACD;AACA,WAAK,YAAY,GAAG;AAAA,IACrB;AAAA,EACD;AAEA,QAAM,IAAI,QAAQ,KAAK;AACvB,SAAO;AACR;AAEA,IAAM,iBAAiB,oBAAI,IAA8B;AACzD,IAAM,cAAc,oBAAI,IAAyB;AAEjD,SAAS,mBAAmB,WAA4C;AACvE,SAAO,YAAY,IAAI,SAAS;AACjC;AAEA,KAAK,YAAY,CAAC,UAA8B;AAC/C,QAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,MAAI,CAAC,MAAM;AACV;AAAA,EACD;AAEA,OAAK,MAAM;AACX,iBAAe,IAAI,MAAM,oBAAI,IAAI,CAAC;AAElC,OAAK,iBAAiB,WAAW,CAAC,iBAAuD;AACxF,UAAM,OAAO,aAAa;AAC1B,QAAI,MAAM,SAAS,YAAY;AAC9B;AAAA,IACD;AAEA,UAAM,OAAO,QAAQ,KAAK,QAAQ,KAAK,SAAS;AAChD,SAAK,QAAQ,IAAI,KAAK,QAAQ,IAAI,KAAK,SAAS;AAChD,gBAAY,IAAI,KAAK,WAAW,IAAI;AACpC,mBAAe,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS;AAE5C,SAAK,OAAO,YAAY,KAAK,OAAO;AAAA,EACrC,CAAC;AAED,OAAK,iBAAiB,gBAAgB,MAAM;AAC3C,UAAM,MAAM,eAAe,IAAI,IAAI;AACnC,QAAI,CAAC,KAAK;AACT;AAAA,IACD;AACA,eAAW,aAAa,KAAK;AAC5B,kBAAY,OAAO,SAAS;AAAA,IAC7B;AACA,mBAAe,OAAO,IAAI;AAAA,EAC3B,CAAC;AACF;","names":[]}
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
- `kora-leader-${dbName}`,
288
- { mode: "exclusive", ifAvailable: true },
289
- (lock) => {
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 timer = setTimeout(() => {
457
+ const settle = (fn) => {
458
+ clearTimeout(timer);
459
+ clearTimeout(probe);
376
460
  this.pending.delete(requestId);
377
- reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs));
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
- clearTimeout(timer);
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.tabSession = await acquireTabStorageSession(this.dbName);
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
- this.bridge = new FollowerBroadcastBridge(
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 });