@invinite-org/chartlang-host-worker 1.0.1

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 (51) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/LICENSE +21 -0
  3. package/README.md +70 -0
  4. package/dist/createWorkerBoot.d.ts +45 -0
  5. package/dist/createWorkerBoot.d.ts.map +1 -0
  6. package/dist/createWorkerBoot.js +122 -0
  7. package/dist/createWorkerBoot.js.map +1 -0
  8. package/dist/createWorkerHost.d.ts +55 -0
  9. package/dist/createWorkerHost.d.ts.map +1 -0
  10. package/dist/createWorkerHost.js +167 -0
  11. package/dist/createWorkerHost.js.map +1 -0
  12. package/dist/defaultWorkerFactory.d.ts +19 -0
  13. package/dist/defaultWorkerFactory.d.ts.map +1 -0
  14. package/dist/defaultWorkerFactory.js +23 -0
  15. package/dist/defaultWorkerFactory.js.map +1 -0
  16. package/dist/filterEmissions.d.ts +21 -0
  17. package/dist/filterEmissions.d.ts.map +1 -0
  18. package/dist/filterEmissions.js +107 -0
  19. package/dist/filterEmissions.js.map +1 -0
  20. package/dist/idb.d.ts +2 -0
  21. package/dist/idb.d.ts.map +1 -0
  22. package/dist/idb.js +4 -0
  23. package/dist/idb.js.map +1 -0
  24. package/dist/idbStateStore.d.ts +22 -0
  25. package/dist/idbStateStore.d.ts.map +1 -0
  26. package/dist/idbStateStore.js +255 -0
  27. package/dist/idbStateStore.js.map +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/limits.d.ts +40 -0
  33. package/dist/limits.d.ts.map +1 -0
  34. package/dist/limits.js +48 -0
  35. package/dist/limits.js.map +1 -0
  36. package/dist/protocol.d.ts +70 -0
  37. package/dist/protocol.d.ts.map +1 -0
  38. package/dist/protocol.js +4 -0
  39. package/dist/protocol.js.map +1 -0
  40. package/dist/types.d.ts +164 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +4 -0
  43. package/dist/types.js.map +1 -0
  44. package/dist/worker-boot.d.ts +6 -0
  45. package/dist/worker-boot.js +14999 -0
  46. package/dist/worker-boot.js.map +7 -0
  47. package/dist/workerBoot.d.ts +2 -0
  48. package/dist/workerBoot.d.ts.map +1 -0
  49. package/dist/workerBoot.js +18 -0
  50. package/dist/workerBoot.js.map +1 -0
  51. package/package.json +57 -0
@@ -0,0 +1,167 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import { defaultWorkerFactory } from "./defaultWorkerFactory.js";
4
+ import { DEFAULT_LIMITS } from "./limits.js";
5
+ function hasTerminate(w) {
6
+ return typeof w.terminate === "function";
7
+ }
8
+ function describeWorkerError(ev) {
9
+ if (typeof ev.message === "string" && ev.message.length > 0)
10
+ return ev.message;
11
+ if (ev.error instanceof Error && ev.error.message.length > 0)
12
+ return ev.error.message;
13
+ if (typeof ev.error === "string" && ev.error.length > 0)
14
+ return ev.error;
15
+ return "unknown worker error";
16
+ }
17
+ /**
18
+ * Build a browser-default `ScriptHost` around a Web Worker. The host
19
+ * round-trips `load` / `push` / `drain` / `dispose` calls across the worker
20
+ * boundary via structured-clone-safe postMessage frames defined in
21
+ * {@link HostToWorker} / {@link WorkerToHost}.
22
+ *
23
+ * The host owns the `nonce` counter for `drain` correlation, the in-flight
24
+ * `load` promise, and the in-flight drain registry. `dispose` posts the
25
+ * tear-down message, calls `terminate()` when the underlying `WorkerLike`
26
+ * supports it, and clears the pending-drain map.
27
+ *
28
+ * @since 0.1
29
+ * @stable
30
+ * @example
31
+ * import { createWorkerHost } from "@invinite-org/chartlang-host-worker";
32
+ * // const host = createWorkerHost({ capabilities });
33
+ * const fn: typeof createWorkerHost = createWorkerHost;
34
+ * void fn;
35
+ */
36
+ export function createWorkerHost(opts) {
37
+ const limits = Object.freeze({ ...DEFAULT_LIMITS, ...opts.limits });
38
+ // The `defaultWorkerFactory` branch is browser-only — tests always inject
39
+ // `workerLike`. Excluded from coverage to keep the production-only
40
+ // `new Worker(...)` path uncounted, consistent with the file-level
41
+ // exclusion of `defaultWorkerFactory.ts` itself.
42
+ /* v8 ignore next */
43
+ const worker = opts.workerLike ?? defaultWorkerFactory();
44
+ let nonceCounter = 0;
45
+ const pendingDrains = new Map();
46
+ let loadedResolve = null;
47
+ let loadedReject = null;
48
+ let loadTimeoutHandle = null;
49
+ // Latches the first worker-level error so a subsequent `load()` call
50
+ // refuses to wait on a dead worker (no `loaded` reply will ever arrive).
51
+ let fatalError = null;
52
+ function clearLoadTimeout() {
53
+ if (loadTimeoutHandle !== null) {
54
+ clearTimeout(loadTimeoutHandle);
55
+ loadTimeoutHandle = null;
56
+ }
57
+ }
58
+ function failLoad(err) {
59
+ clearLoadTimeout();
60
+ loadedReject?.(err);
61
+ loadedResolve = null;
62
+ loadedReject = null;
63
+ }
64
+ worker.addEventListener("message", (ev) => {
65
+ const msg = ev.data;
66
+ switch (msg.kind) {
67
+ case "loaded": {
68
+ clearLoadTimeout();
69
+ loadedResolve?.();
70
+ loadedResolve = null;
71
+ loadedReject = null;
72
+ break;
73
+ }
74
+ case "loadError": {
75
+ failLoad(new Error(msg.message));
76
+ break;
77
+ }
78
+ case "emissions": {
79
+ const resolve = pendingDrains.get(msg.nonce);
80
+ if (resolve !== undefined) {
81
+ pendingDrains.delete(msg.nonce);
82
+ resolve(msg.emissions);
83
+ }
84
+ break;
85
+ }
86
+ case "step-overshoot": {
87
+ opts.onWorkerError?.(`step overshoot ${msg.observedMs.toFixed(2)}ms`);
88
+ break;
89
+ }
90
+ case "fatal": {
91
+ opts.onWorkerError?.(msg.message);
92
+ break;
93
+ }
94
+ }
95
+ });
96
+ // The error channel is fed by browser `Worker`'s `onerror` event.
97
+ // `MessagePort`-backed fakes accept the subscription silently and never
98
+ // fire it, which is the right behaviour: a port doesn't have its own
99
+ // boot/error channel. The cast narrows `addEventListener`'s overload
100
+ // signature to the error variant.
101
+ const addErrorListener = worker.addEventListener;
102
+ addErrorListener("error", (ev) => {
103
+ const description = describeWorkerError(ev);
104
+ fatalError = description;
105
+ const message = `worker failed to boot: ${description}`;
106
+ failLoad(new Error(message));
107
+ opts.onWorkerError?.(message);
108
+ });
109
+ return Object.freeze({
110
+ load(compiled) {
111
+ return new Promise((resolve, reject) => {
112
+ if (fatalError !== null) {
113
+ reject(new Error(`worker failed to boot: ${fatalError}`));
114
+ return;
115
+ }
116
+ if (loadedResolve !== null) {
117
+ reject(new Error("load() already in flight"));
118
+ return;
119
+ }
120
+ loadedResolve = resolve;
121
+ loadedReject = reject;
122
+ loadTimeoutHandle = setTimeout(() => {
123
+ failLoad(new Error(`worker load() timed out after ${limits.maxLoadTimeoutMs}ms — worker never replied with 'loaded'`));
124
+ }, limits.maxLoadTimeoutMs);
125
+ const frame = {
126
+ kind: "load",
127
+ compiled: {
128
+ moduleSource: compiled.moduleSource,
129
+ manifest: compiled.manifest,
130
+ },
131
+ capabilities: opts.capabilities,
132
+ ...(opts.symInfo !== undefined ? { symInfo: opts.symInfo } : {}),
133
+ ...(opts.resolveInputs !== undefined
134
+ ? { inputOverrides: opts.resolveInputs(compiled.manifest.name) }
135
+ : {}),
136
+ limits,
137
+ };
138
+ worker.postMessage(frame);
139
+ });
140
+ },
141
+ push(event) {
142
+ const frame = { kind: "candleEvent", event };
143
+ worker.postMessage(frame);
144
+ return Promise.resolve();
145
+ },
146
+ drain() {
147
+ const n = nonceCounter;
148
+ nonceCounter += 1;
149
+ return new Promise((resolve) => {
150
+ pendingDrains.set(n, resolve);
151
+ const frame = { kind: "drain", nonce: n };
152
+ worker.postMessage(frame);
153
+ });
154
+ },
155
+ dispose() {
156
+ const frame = { kind: "dispose" };
157
+ worker.postMessage(frame);
158
+ if (hasTerminate(worker)) {
159
+ worker.terminate();
160
+ }
161
+ clearLoadTimeout();
162
+ pendingDrains.clear();
163
+ },
164
+ limits,
165
+ });
166
+ }
167
+ //# sourceMappingURL=createWorkerHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createWorkerHost.js","sourceRoot":"","sources":["../src/createWorkerHost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { WorkerLike } from "./types.js";
2
+ /**
3
+ * Browser-only fallback when `createWorkerHost` is called without an
4
+ * explicit `workerLike`. Constructs a real `Worker` against the bundled
5
+ * `dist/worker-boot.js` sibling. In a real Node test runner `Worker` is not
6
+ * a global so calling this throws a `ReferenceError`; tests always inject a
7
+ * `MessageChannel`-backed `WorkerLike` instead, and the coverage test in
8
+ * `defaultWorkerFactory.test.ts` stubs `globalThis.Worker` to exercise the
9
+ * construction path.
10
+ *
11
+ * @since 0.1
12
+ * @stable
13
+ * @example
14
+ * // const worker = defaultWorkerFactory(); // browser only
15
+ * const fn: typeof defaultWorkerFactory = defaultWorkerFactory;
16
+ * void fn;
17
+ */
18
+ export declare function defaultWorkerFactory(): WorkerLike;
19
+ //# sourceMappingURL=defaultWorkerFactory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultWorkerFactory.d.ts","sourceRoot":"","sources":["../src/defaultWorkerFactory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAGjD"}
@@ -0,0 +1,23 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ /**
4
+ * Browser-only fallback when `createWorkerHost` is called without an
5
+ * explicit `workerLike`. Constructs a real `Worker` against the bundled
6
+ * `dist/worker-boot.js` sibling. In a real Node test runner `Worker` is not
7
+ * a global so calling this throws a `ReferenceError`; tests always inject a
8
+ * `MessageChannel`-backed `WorkerLike` instead, and the coverage test in
9
+ * `defaultWorkerFactory.test.ts` stubs `globalThis.Worker` to exercise the
10
+ * construction path.
11
+ *
12
+ * @since 0.1
13
+ * @stable
14
+ * @example
15
+ * // const worker = defaultWorkerFactory(); // browser only
16
+ * const fn: typeof defaultWorkerFactory = defaultWorkerFactory;
17
+ * void fn;
18
+ */
19
+ export function defaultWorkerFactory() {
20
+ const url = new URL("./worker-boot.js", import.meta.url);
21
+ return new Worker(url, { type: "module" });
22
+ }
23
+ //# sourceMappingURL=defaultWorkerFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultWorkerFactory.js","sourceRoot":"","sources":["../src/defaultWorkerFactory.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAI/D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,oBAAoB;IAChC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,kBAAkB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzD,OAAO,IAAI,MAAM,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type RunnerEmissions } from "@invinite-org/chartlang-adapter-kit";
2
+ /**
3
+ * Walk a `RunnerEmissions` snapshot and replace any plot / alert that fails
4
+ * adapter-kit's `validateEmission` with a `malformed-emission` diagnostic.
5
+ * Drawings pass through unchanged in Phase 1 (no `draw.*` primitives ship
6
+ * yet); diagnostics are appended to (never validated against — recursive
7
+ * validation would loop).
8
+ *
9
+ * The boot calls this on every `drain()` before posting `emissions` back to
10
+ * the host; the trust boundary for the postMessage wire format is here.
11
+ *
12
+ * @since 0.1
13
+ * @stable
14
+ * @example
15
+ * // const out = filterEmissions(runner.drain());
16
+ * // postMessage({ kind: "emissions", nonce, emissions: out });
17
+ * const fn: typeof filterEmissions = filterEmissions;
18
+ * void fn;
19
+ */
20
+ export declare function filterEmissions(raw: RunnerEmissions): RunnerEmissions;
21
+ //# sourceMappingURL=filterEmissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterEmissions.d.ts","sourceRoot":"","sources":["../src/filterEmissions.ts"],"names":[],"mappings":"AAGA,OAAO,EAMH,KAAK,eAAe,EAGvB,MAAM,qCAAqC,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,eAAe,CAiFrE"}
@@ -0,0 +1,107 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import { validateEmission, } from "@invinite-org/chartlang-adapter-kit";
4
+ /**
5
+ * Walk a `RunnerEmissions` snapshot and replace any plot / alert that fails
6
+ * adapter-kit's `validateEmission` with a `malformed-emission` diagnostic.
7
+ * Drawings pass through unchanged in Phase 1 (no `draw.*` primitives ship
8
+ * yet); diagnostics are appended to (never validated against — recursive
9
+ * validation would loop).
10
+ *
11
+ * The boot calls this on every `drain()` before posting `emissions` back to
12
+ * the host; the trust boundary for the postMessage wire format is here.
13
+ *
14
+ * @since 0.1
15
+ * @stable
16
+ * @example
17
+ * // const out = filterEmissions(runner.drain());
18
+ * // postMessage({ kind: "emissions", nonce, emissions: out });
19
+ * const fn: typeof filterEmissions = filterEmissions;
20
+ * void fn;
21
+ */
22
+ export function filterEmissions(raw) {
23
+ const plots = [];
24
+ const drawings = [];
25
+ const alerts = [];
26
+ const alertConditions = [];
27
+ const logs = [];
28
+ const diagnostics = [...raw.diagnostics];
29
+ for (const p of raw.plots) {
30
+ const r = validateEmission(p);
31
+ if (r.ok) {
32
+ plots.push(p);
33
+ }
34
+ else {
35
+ diagnostics.push({
36
+ kind: "diagnostic",
37
+ severity: "warning",
38
+ code: r.code,
39
+ message: r.message,
40
+ slotId: p.slotId,
41
+ bar: p.bar,
42
+ });
43
+ }
44
+ }
45
+ for (const a of raw.alerts) {
46
+ const r = validateEmission(a);
47
+ if (r.ok) {
48
+ alerts.push(a);
49
+ }
50
+ else {
51
+ diagnostics.push({
52
+ kind: "diagnostic",
53
+ severity: "warning",
54
+ code: r.code,
55
+ message: r.message,
56
+ slotId: a.slotId,
57
+ bar: a.bar,
58
+ });
59
+ }
60
+ }
61
+ for (const condition of raw.alertConditions) {
62
+ const r = validateEmission(condition);
63
+ if (r.ok) {
64
+ alertConditions.push(condition);
65
+ }
66
+ else {
67
+ diagnostics.push({
68
+ kind: "diagnostic",
69
+ severity: "warning",
70
+ code: r.code,
71
+ message: r.message,
72
+ slotId: null,
73
+ bar: condition.bar,
74
+ });
75
+ }
76
+ }
77
+ for (const log of raw.logs) {
78
+ const r = validateEmission(log);
79
+ if (r.ok) {
80
+ logs.push(log);
81
+ }
82
+ else {
83
+ diagnostics.push({
84
+ kind: "diagnostic",
85
+ severity: "warning",
86
+ code: r.code,
87
+ message: r.message,
88
+ slotId: null,
89
+ bar: log.bar,
90
+ });
91
+ }
92
+ }
93
+ for (const d of raw.drawings) {
94
+ drawings.push(d);
95
+ }
96
+ return {
97
+ plots,
98
+ drawings,
99
+ alerts,
100
+ alertConditions,
101
+ logs,
102
+ diagnostics,
103
+ fromBar: raw.fromBar,
104
+ toBar: raw.toBar,
105
+ };
106
+ }
107
+ //# sourceMappingURL=filterEmissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterEmissions.js","sourceRoot":"","sources":["../src/filterEmissions.ts"],"names":[],"mappings":"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"}
package/dist/idb.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { idbStateStore } from "./idbStateStore.js";
2
+ //# sourceMappingURL=idb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb.d.ts","sourceRoot":"","sources":["../src/idb.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/idb.js ADDED
@@ -0,0 +1,4 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ export { idbStateStore } from "./idbStateStore.js";
4
+ //# sourceMappingURL=idb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb.js","sourceRoot":"","sources":["../src/idb.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { StateStoreKey } from "@invinite-org/chartlang-core";
2
+ import type { PersistentStateStore } from "@invinite-org/chartlang-runtime";
3
+ /**
4
+ * IDB-backed {@link PersistentStateStore}. One record per `StateStoreKey`,
5
+ * capped at `capBytes` total with oldest-first eviction by `snapshot.savedAt`.
6
+ * Reads on mount, writes on dispose + 60s cadence; the cadence is enforced by
7
+ * the runtime, not the store. Identity-safe: `load()` returns `null` for any
8
+ * non-matching key, and `clear()` deletes only this key's record.
9
+ *
10
+ * @since 0.5
11
+ * @stable
12
+ * @example
13
+ * // import { idbStateStore } from "@invinite-org/chartlang-host-worker/idb";
14
+ * // const store = idbStateStore({ dbName: "chartlang", key });
15
+ * // await store.save(snapshot);
16
+ */
17
+ export declare function idbStateStore(opts: Readonly<{
18
+ key: StateStoreKey;
19
+ dbName?: string;
20
+ capBytes?: number;
21
+ }>): PersistentStateStore;
22
+ //# sourceMappingURL=idbStateStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idbStateStore.d.ts","sourceRoot":"","sources":["../src/idbStateStore.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACjF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAgB5E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,QAAQ,CAAC;IACX,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC,GACH,oBAAoB,CA2CtB"}
@@ -0,0 +1,255 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ const DEFAULT_DB_NAME = "chartlang";
4
+ const DEFAULT_CAP_BYTES = 50 * 1024 * 1024;
5
+ const OBJECT_STORE = "chartlangSnapshots";
6
+ const SAVED_AT_INDEX = "savedAt";
7
+ const dbPromises = new Map();
8
+ /**
9
+ * IDB-backed {@link PersistentStateStore}. One record per `StateStoreKey`,
10
+ * capped at `capBytes` total with oldest-first eviction by `snapshot.savedAt`.
11
+ * Reads on mount, writes on dispose + 60s cadence; the cadence is enforced by
12
+ * the runtime, not the store. Identity-safe: `load()` returns `null` for any
13
+ * non-matching key, and `clear()` deletes only this key's record.
14
+ *
15
+ * @since 0.5
16
+ * @stable
17
+ * @example
18
+ * // import { idbStateStore } from "@invinite-org/chartlang-host-worker/idb";
19
+ * // const store = idbStateStore({ dbName: "chartlang", key });
20
+ * // await store.save(snapshot);
21
+ */
22
+ export function idbStateStore(opts) {
23
+ const dbName = opts.dbName ?? DEFAULT_DB_NAME;
24
+ const capBytes = opts.capBytes ?? DEFAULT_CAP_BYTES;
25
+ const keyString = stringifyKey(opts.key);
26
+ return Object.freeze({
27
+ key: opts.key,
28
+ async load() {
29
+ const db = await openDb(dbName);
30
+ const tx = db.transaction(OBJECT_STORE, "readonly");
31
+ const done = transactionDone(tx);
32
+ const value = await requestToPromise(tx.objectStore(OBJECT_STORE).get(keyString));
33
+ await done;
34
+ return isStoredRecord(value) ? value.snapshot : null;
35
+ },
36
+ async save(snapshot) {
37
+ const db = await openDb(dbName);
38
+ const tx = db.transaction(OBJECT_STORE, "readwrite");
39
+ const done = transactionDone(tx);
40
+ const store = tx.objectStore(OBJECT_STORE);
41
+ const records = await readAllRecords(store);
42
+ const nextBytes = estimateSnapshotBytes(snapshot);
43
+ await evictUntilUnderCap(store, records, keyString, nextBytes, capBytes);
44
+ await requestToPromise(store.put({
45
+ keyString,
46
+ snapshot,
47
+ bytesEstimate: nextBytes,
48
+ savedAt: snapshot.savedAt,
49
+ }));
50
+ await done;
51
+ },
52
+ async clear() {
53
+ const db = await openDb(dbName);
54
+ const tx = db.transaction(OBJECT_STORE, "readwrite");
55
+ const done = transactionDone(tx);
56
+ await requestToPromise(tx.objectStore(OBJECT_STORE).delete(keyString));
57
+ await done;
58
+ },
59
+ });
60
+ }
61
+ /**
62
+ * Stable cache-key serialisation for PLAN.md §6.9 `StateStoreKey`.
63
+ *
64
+ * @internal
65
+ */
66
+ function stringifyKey(key) {
67
+ return JSON.stringify({
68
+ scriptHash: key.scriptHash,
69
+ compilerVersion: key.compilerVersion,
70
+ apiVersion: key.apiVersion,
71
+ capabilitiesHash: key.capabilitiesHash,
72
+ symbol: key.symbol,
73
+ mainInterval: key.mainInterval,
74
+ requestedIntervals: key.requestedIntervals.join(","),
75
+ });
76
+ }
77
+ /**
78
+ * Lazy singleton IDB connection per database name.
79
+ *
80
+ * @internal
81
+ */
82
+ function openDb(dbName) {
83
+ const existing = dbPromises.get(dbName);
84
+ if (existing !== undefined)
85
+ return existing;
86
+ const promise = new Promise((resolve, reject) => {
87
+ if (globalThis.indexedDB === undefined) {
88
+ reject(new Error("indexedDB is not available"));
89
+ return;
90
+ }
91
+ let request;
92
+ try {
93
+ request = globalThis.indexedDB.open(dbName, 1);
94
+ }
95
+ catch (error) {
96
+ reject(toError(error, "indexedDB.open failed"));
97
+ return;
98
+ }
99
+ request.onupgradeneeded = () => {
100
+ const db = request.result;
101
+ if (!db.objectStoreNames.contains(OBJECT_STORE)) {
102
+ const store = db.createObjectStore(OBJECT_STORE, { keyPath: "keyString" });
103
+ store.createIndex(SAVED_AT_INDEX, "savedAt");
104
+ }
105
+ };
106
+ request.onerror = () => {
107
+ reject(toError(request.error, "indexedDB.open failed"));
108
+ };
109
+ request.onblocked = () => {
110
+ reject(new Error(`indexedDB.open blocked for database "${dbName}"`));
111
+ };
112
+ request.onsuccess = () => {
113
+ const db = request.result;
114
+ // Drop the cached promise if the connection is closed or another
115
+ // tab requests a version upgrade, so the next openDb() reopens a
116
+ // valid connection instead of reusing a dead IDBDatabase.
117
+ const evict = () => {
118
+ if (dbPromises.get(dbName) === promise)
119
+ dbPromises.delete(dbName);
120
+ };
121
+ db.onclose = evict;
122
+ db.onversionchange = () => {
123
+ db.close();
124
+ evict();
125
+ };
126
+ resolve(db);
127
+ };
128
+ });
129
+ promise.catch(() => {
130
+ if (dbPromises.get(dbName) === promise)
131
+ dbPromises.delete(dbName);
132
+ });
133
+ dbPromises.set(dbName, promise);
134
+ return promise;
135
+ }
136
+ /**
137
+ * Promise wrapper for one IDB request.
138
+ *
139
+ * @internal
140
+ */
141
+ function requestToPromise(request) {
142
+ return new Promise((resolve, reject) => {
143
+ request.onerror = () => {
144
+ reject(toError(request.error, "IndexedDB request failed"));
145
+ };
146
+ request.onsuccess = () => {
147
+ resolve(request.result);
148
+ };
149
+ });
150
+ }
151
+ /**
152
+ * Resolves only when the whole IDB transaction completes.
153
+ *
154
+ * @internal
155
+ */
156
+ function transactionDone(tx) {
157
+ return new Promise((resolve, reject) => {
158
+ tx.onabort = () => {
159
+ reject(toError(tx.error, "IndexedDB transaction aborted"));
160
+ };
161
+ tx.onerror = () => {
162
+ reject(toError(tx.error, "IndexedDB transaction failed"));
163
+ };
164
+ tx.oncomplete = () => {
165
+ resolve();
166
+ };
167
+ });
168
+ }
169
+ /**
170
+ * Reads all well-formed snapshot records from the store.
171
+ *
172
+ * @internal
173
+ */
174
+ function readAllRecords(store) {
175
+ return new Promise((resolve, reject) => {
176
+ const records = [];
177
+ const request = store.index(SAVED_AT_INDEX).openCursor();
178
+ request.onerror = () => {
179
+ reject(toError(request.error, "IndexedDB cursor failed"));
180
+ };
181
+ request.onsuccess = () => {
182
+ const cursor = request.result;
183
+ if (cursor === null) {
184
+ resolve(records);
185
+ return;
186
+ }
187
+ if (isStoredRecord(cursor.value))
188
+ records.push(cursor.value);
189
+ cursor.continue();
190
+ };
191
+ });
192
+ }
193
+ /**
194
+ * Evicts savedAt-ascending records until the pending write fits the cap.
195
+ *
196
+ * @internal
197
+ */
198
+ async function evictUntilUnderCap(store, records, keyString, nextBytes, capBytes) {
199
+ let total = 0;
200
+ const candidates = [];
201
+ for (const record of records) {
202
+ if (record.keyString === keyString)
203
+ continue;
204
+ total += record.bytesEstimate;
205
+ candidates.push(record);
206
+ }
207
+ candidates.sort((a, b) => a.savedAt - b.savedAt);
208
+ for (const record of candidates) {
209
+ if (total + nextBytes <= capBytes)
210
+ return;
211
+ await requestToPromise(store.delete(record.keyString));
212
+ total -= record.bytesEstimate;
213
+ }
214
+ }
215
+ /**
216
+ * UTF-16 byte estimate used for best-effort cap accounting.
217
+ *
218
+ * @internal
219
+ */
220
+ function estimateSnapshotBytes(snapshot) {
221
+ return JSON.stringify(snapshot).length * 2;
222
+ }
223
+ /**
224
+ * Narrows records read from IDB's untyped structured-clone surface.
225
+ *
226
+ * @internal
227
+ */
228
+ function isStoredRecord(value) {
229
+ if (typeof value !== "object" || value === null)
230
+ return false;
231
+ if (!("keyString" in value) || typeof value.keyString !== "string")
232
+ return false;
233
+ if (!("bytesEstimate" in value) || typeof value.bytesEstimate !== "number")
234
+ return false;
235
+ if (!("savedAt" in value) || typeof value.savedAt !== "number")
236
+ return false;
237
+ if (!("snapshot" in value))
238
+ return false;
239
+ return true;
240
+ }
241
+ /**
242
+ * Preserves underlying IDB messages on rejection paths.
243
+ *
244
+ * @internal
245
+ */
246
+ function toError(value, fallback) {
247
+ if (value instanceof DOMException)
248
+ return new Error(value.message);
249
+ if (value instanceof Error)
250
+ return value;
251
+ if (typeof value === "string")
252
+ return new Error(value);
253
+ return new Error(fallback);
254
+ }
255
+ //# sourceMappingURL=idbStateStore.js.map