@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.
- package/CHANGELOG.md +228 -0
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/createWorkerBoot.d.ts +45 -0
- package/dist/createWorkerBoot.d.ts.map +1 -0
- package/dist/createWorkerBoot.js +122 -0
- package/dist/createWorkerBoot.js.map +1 -0
- package/dist/createWorkerHost.d.ts +55 -0
- package/dist/createWorkerHost.d.ts.map +1 -0
- package/dist/createWorkerHost.js +167 -0
- package/dist/createWorkerHost.js.map +1 -0
- package/dist/defaultWorkerFactory.d.ts +19 -0
- package/dist/defaultWorkerFactory.d.ts.map +1 -0
- package/dist/defaultWorkerFactory.js +23 -0
- package/dist/defaultWorkerFactory.js.map +1 -0
- package/dist/filterEmissions.d.ts +21 -0
- package/dist/filterEmissions.d.ts.map +1 -0
- package/dist/filterEmissions.js +107 -0
- package/dist/filterEmissions.js.map +1 -0
- package/dist/idb.d.ts +2 -0
- package/dist/idb.d.ts.map +1 -0
- package/dist/idb.js +4 -0
- package/dist/idb.js.map +1 -0
- package/dist/idbStateStore.d.ts +22 -0
- package/dist/idbStateStore.d.ts.map +1 -0
- package/dist/idbStateStore.js +255 -0
- package/dist/idbStateStore.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/limits.d.ts +40 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +48 -0
- package/dist/limits.js.map +1 -0
- package/dist/protocol.d.ts +70 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/worker-boot.d.ts +6 -0
- package/dist/worker-boot.js +14999 -0
- package/dist/worker-boot.js.map +7 -0
- package/dist/workerBoot.d.ts +2 -0
- package/dist/workerBoot.d.ts.map +1 -0
- package/dist/workerBoot.js +18 -0
- package/dist/workerBoot.js.map +1 -0
- 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 @@
|
|
|
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
package/dist/idb.js.map
ADDED
|
@@ -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
|