@jigs.design/runtime 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/frame-policy.d.ts +11 -0
- package/dist/frame-policy.js +17 -0
- package/dist/frame.d.ts +42 -0
- package/dist/frame.js +258 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +150 -0
- package/dist/protocol.d.ts +132 -0
- package/dist/protocol.js +4 -0
- package/dist/runtime-core.d.ts +21 -0
- package/dist/runtime-core.js +735 -0
- package/dist/tree-store.d.ts +56 -0
- package/dist/tree-store.js +340 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +22 -0
- package/dist/urls.d.ts +14 -0
- package/dist/urls.js +59 -0
- package/dist/validate.js +33 -0
- package/package.json +28 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Bates
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jigs.design/runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The layer the server and the browser share: the program format, the types on the wire, the sandboxed runtime, and the tree store. It imports neither the compiler nor the prompt.
|
|
4
|
+
|
|
5
|
+
`@jigs.design/server`, `@jigs.design/client` and `@jigs.design/react` re-export the shared types, so most code imports them from one of those.
|
|
6
|
+
|
|
7
|
+
## Entry points
|
|
8
|
+
|
|
9
|
+
| Entry | What it holds |
|
|
10
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `@jigs.design/runtime` | `Program`, `SessionEvent`, `Message`, `Diagnostic` and the other shared types; the frame protocol (`HostMessage`, `FrameMessage`, `Op`); the JSON checks the browser runs; `createTreeStore` with its caps `MAX_NODES`, `MAX_OPS` and `MAX_TEXT`; the image and link allowlists |
|
|
12
|
+
| `@jigs.design/runtime/frame` | The sandbox frame document, its content security policy, the worker runtime, and `WATCHDOG_MS` and `HEARTBEAT_MS`. The server serves it |
|
|
13
|
+
|
|
14
|
+
## How a program runs
|
|
15
|
+
|
|
16
|
+
The server serves the frame document from its route. The frame starts a worker and runs the compiled program there, with a watchdog that kills a program that stops answering. The worker sends tree ops to the page, where the tree store applies them, within the same caps on nodes, ops and text the worker keeps, and the view draws the tree. Functions never leave the worker; they cross as ids. The browser checks every value from the server with `parse.ts`, since it imports no schema library.
|
|
17
|
+
|
|
18
|
+
## Source
|
|
19
|
+
|
|
20
|
+
- `src/types.ts`: the shared types and `PROGRAM_FORMAT`.
|
|
21
|
+
- `src/protocol.ts`: messages between the page, the frame and the worker.
|
|
22
|
+
- `src/parse.ts`: checks for JSON arriving in the browser.
|
|
23
|
+
- `src/frame.ts`, `src/frame-policy.ts`: the frame document and its policy.
|
|
24
|
+
- `src/runtime-core.ts`: the runtime inside the worker.
|
|
25
|
+
- `src/tree-store.ts`: the tree the view draws.
|
|
26
|
+
- `src/urls.ts`, `src/validate.ts`: allowed images and links, and prop checks.
|
|
27
|
+
|
|
28
|
+
## Docs
|
|
29
|
+
|
|
30
|
+
[Sandbox](../../apps/docs/docs/react/sandbox.mdx), [Frame protocol](../../apps/docs/docs/renderers/protocol.mdx) and [Packages](../../apps/docs/docs/packages/index.mdx).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/frame-policy.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The sandbox frame's content security policy, without `frame-ancestors`.
|
|
4
|
+
* The `sandbox` directive keeps the document's origin opaque even when a page
|
|
5
|
+
* frames it without the attribute, and `default-src 'none'` covers every
|
|
6
|
+
* fetch directive not listed, so nothing in the frame can make a request.
|
|
7
|
+
* The frame checks that this policy is the one in force before it runs
|
|
8
|
+
* anything, and the view looks for it when the frame does not start.
|
|
9
|
+
*/
|
|
10
|
+
export declare const frameSecurityPolicy: string;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/frame-policy.ts
|
|
2
|
+
/**
|
|
3
|
+
* The sandbox frame's content security policy, without `frame-ancestors`.
|
|
4
|
+
* The `sandbox` directive keeps the document's origin opaque even when a page
|
|
5
|
+
* frames it without the attribute, and `default-src 'none'` covers every
|
|
6
|
+
* fetch directive not listed, so nothing in the frame can make a request.
|
|
7
|
+
* The frame checks that this policy is the one in force before it runs
|
|
8
|
+
* anything, and the view looks for it when the frame does not start.
|
|
9
|
+
*/
|
|
10
|
+
const frameSecurityPolicy = [
|
|
11
|
+
"sandbox allow-scripts",
|
|
12
|
+
"default-src 'none'",
|
|
13
|
+
"script-src 'unsafe-inline' 'unsafe-eval'",
|
|
14
|
+
"worker-src blob:"
|
|
15
|
+
].join("; ");
|
|
16
|
+
//#endregion
|
|
17
|
+
export { frameSecurityPolicy };
|
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { frameSecurityPolicy } from "./frame-policy.js";
|
|
2
|
+
import { createRuntime } from "./runtime-core.js";
|
|
3
|
+
//#region src/frame.d.ts
|
|
4
|
+
/** How long a program may take to answer before the worker is killed. */
|
|
5
|
+
export declare const WATCHDOG_MS = 1000;
|
|
6
|
+
/**
|
|
7
|
+
* How long a worker that ran a program may sit idle before the runner pings
|
|
8
|
+
* it, under the same watchdog. A program can start an endless microtask
|
|
9
|
+
* chain after its reply has gone, from an API that resolves in a task of its
|
|
10
|
+
* own (`Blob.arrayBuffer()`, `crypto.subtle`); the ping catches that spin
|
|
11
|
+
* without waiting for the user's next click. Pings stop while the page is
|
|
12
|
+
* hidden and before the worker has had a program.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HEARTBEAT_MS = 2000;
|
|
15
|
+
/** What the policy check needs from the frame: its document's events, `fetch` and a timer. */
|
|
16
|
+
export interface PolicyScope {
|
|
17
|
+
addEventListener: (type: "securitypolicyviolation", listener: (event: {
|
|
18
|
+
originalPolicy: string;
|
|
19
|
+
}) => void) => void;
|
|
20
|
+
fetch: (url: string) => Promise<unknown>;
|
|
21
|
+
removeEventListener: (type: "securitypolicyviolation", listener: (event: {
|
|
22
|
+
originalPolicy: string;
|
|
23
|
+
}) => void) => void;
|
|
24
|
+
setTimeout: (callback: () => void, ms: number) => unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves true when the policy in force on the frame starts with `expected`.
|
|
28
|
+
* A middleware that sets the page's policy on every response replaces the
|
|
29
|
+
* frame's, and the frame would then run without its network lock. The probe
|
|
30
|
+
* fetches a `data:` URL, which makes no request: the frame's policy refuses
|
|
31
|
+
* it and reports a violation that carries the policy's text. If the fetch
|
|
32
|
+
* succeeds, nothing restricts it; if only other policies report it, the
|
|
33
|
+
* frame's is gone. Serialized with `toString()`, so it must stay
|
|
34
|
+
* self-contained.
|
|
35
|
+
*/
|
|
36
|
+
export declare function framePolicyInForce(scope: PolicyScope, expected: string, waitMs: number): Promise<boolean>;
|
|
37
|
+
/** Source of the sandbox worker: the runtime, booted with network and code loading removed. */
|
|
38
|
+
export declare const workerSource: string;
|
|
39
|
+
/** The sandbox frame document the server sends on `GET`. */
|
|
40
|
+
export declare function frameDocument(): string;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { createRuntime, frameSecurityPolicy };
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { frameSecurityPolicy } from "./frame-policy.js";
|
|
2
|
+
import { createRuntime } from "./runtime-core.js";
|
|
3
|
+
//#region src/frame.ts
|
|
4
|
+
/** How long a program may take to answer before the worker is killed. */
|
|
5
|
+
const WATCHDOG_MS = 1e3;
|
|
6
|
+
/**
|
|
7
|
+
* How long a worker that ran a program may sit idle before the runner pings
|
|
8
|
+
* it, under the same watchdog. A program can start an endless microtask
|
|
9
|
+
* chain after its reply has gone, from an API that resolves in a task of its
|
|
10
|
+
* own (`Blob.arrayBuffer()`, `crypto.subtle`); the ping catches that spin
|
|
11
|
+
* without waiting for the user's next click. Pings stop while the page is
|
|
12
|
+
* hidden and before the worker has had a program.
|
|
13
|
+
*/
|
|
14
|
+
const HEARTBEAT_MS = 2e3;
|
|
15
|
+
/** Worker globals removed before any program runs: network, timers, messaging, storage, code loading. */
|
|
16
|
+
const LOCKED_GLOBALS = [
|
|
17
|
+
"fetch",
|
|
18
|
+
"XMLHttpRequest",
|
|
19
|
+
"WebSocket",
|
|
20
|
+
"WebTransport",
|
|
21
|
+
"EventSource",
|
|
22
|
+
"importScripts",
|
|
23
|
+
"setTimeout",
|
|
24
|
+
"setInterval",
|
|
25
|
+
"clearTimeout",
|
|
26
|
+
"clearInterval",
|
|
27
|
+
"requestAnimationFrame",
|
|
28
|
+
"queueMicrotask",
|
|
29
|
+
"Worker",
|
|
30
|
+
"SharedWorker",
|
|
31
|
+
"BroadcastChannel",
|
|
32
|
+
"MessageChannel",
|
|
33
|
+
"MessagePort",
|
|
34
|
+
"postMessage",
|
|
35
|
+
"indexedDB",
|
|
36
|
+
"caches",
|
|
37
|
+
"navigator",
|
|
38
|
+
"location",
|
|
39
|
+
"eval",
|
|
40
|
+
"WebAssembly",
|
|
41
|
+
"Function",
|
|
42
|
+
"Proxy",
|
|
43
|
+
"Reflect",
|
|
44
|
+
"WeakRef",
|
|
45
|
+
"FinalizationRegistry",
|
|
46
|
+
"Atomics"
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Worker entry point. Serialized with `toString()` like `createRuntime`, so
|
|
50
|
+
* it must stay self-contained.
|
|
51
|
+
*/
|
|
52
|
+
function bootWorker(create, locked) {
|
|
53
|
+
const scope = globalThis;
|
|
54
|
+
const post = scope.postMessage.bind(scope);
|
|
55
|
+
const later = scope.setTimeout.bind(scope);
|
|
56
|
+
const runtime = create((message) => {
|
|
57
|
+
later(() => post(message), 0);
|
|
58
|
+
}, Function);
|
|
59
|
+
const isPing = (data) => typeof data === "object" && data !== null && "type" in data && data.type === "ping";
|
|
60
|
+
scope.addEventListener("message", (event) => {
|
|
61
|
+
const data = event.data;
|
|
62
|
+
if (isPing(data)) {
|
|
63
|
+
later(() => post(data), 0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
runtime.handle(event.data);
|
|
67
|
+
});
|
|
68
|
+
for (const fn of [
|
|
69
|
+
() => void 0,
|
|
70
|
+
async () => void 0,
|
|
71
|
+
function* () {
|
|
72
|
+
yield;
|
|
73
|
+
},
|
|
74
|
+
async function* () {
|
|
75
|
+
yield;
|
|
76
|
+
}
|
|
77
|
+
]) Object.defineProperty(Object.getPrototypeOf(fn), "constructor", {
|
|
78
|
+
configurable: false,
|
|
79
|
+
value: void 0,
|
|
80
|
+
writable: false
|
|
81
|
+
});
|
|
82
|
+
for (const builtin of [
|
|
83
|
+
Object,
|
|
84
|
+
Array,
|
|
85
|
+
Function,
|
|
86
|
+
String,
|
|
87
|
+
Number,
|
|
88
|
+
Boolean,
|
|
89
|
+
Symbol,
|
|
90
|
+
Map,
|
|
91
|
+
Set,
|
|
92
|
+
Promise,
|
|
93
|
+
RegExp,
|
|
94
|
+
Date,
|
|
95
|
+
Error
|
|
96
|
+
]) {
|
|
97
|
+
Object.freeze(builtin.prototype);
|
|
98
|
+
Object.freeze(builtin);
|
|
99
|
+
}
|
|
100
|
+
Object.freeze(Math);
|
|
101
|
+
Object.freeze(JSON);
|
|
102
|
+
for (const name of locked) {
|
|
103
|
+
let target = globalThis;
|
|
104
|
+
while (target !== null) {
|
|
105
|
+
if (Object.hasOwn(target, name)) try {
|
|
106
|
+
Object.defineProperty(target, name, {
|
|
107
|
+
configurable: false,
|
|
108
|
+
value: void 0,
|
|
109
|
+
writable: false
|
|
110
|
+
});
|
|
111
|
+
} catch {}
|
|
112
|
+
target = Object.getPrototypeOf(target);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** How long the frame waits for its own policy to report the probe. */
|
|
117
|
+
const POLICY_WAIT_MS = 1e3;
|
|
118
|
+
/**
|
|
119
|
+
* Resolves true when the policy in force on the frame starts with `expected`.
|
|
120
|
+
* A middleware that sets the page's policy on every response replaces the
|
|
121
|
+
* frame's, and the frame would then run without its network lock. The probe
|
|
122
|
+
* fetches a `data:` URL, which makes no request: the frame's policy refuses
|
|
123
|
+
* it and reports a violation that carries the policy's text. If the fetch
|
|
124
|
+
* succeeds, nothing restricts it; if only other policies report it, the
|
|
125
|
+
* frame's is gone. Serialized with `toString()`, so it must stay
|
|
126
|
+
* self-contained.
|
|
127
|
+
*/
|
|
128
|
+
function framePolicyInForce(scope, expected, waitMs) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const onViolation = (event) => {
|
|
131
|
+
if (event.originalPolicy.startsWith(expected)) finish(true);
|
|
132
|
+
};
|
|
133
|
+
const finish = (inForce) => {
|
|
134
|
+
scope.removeEventListener("securitypolicyviolation", onViolation);
|
|
135
|
+
resolve(inForce);
|
|
136
|
+
};
|
|
137
|
+
scope.addEventListener("securitypolicyviolation", onViolation);
|
|
138
|
+
scope.fetch("data:,").then(() => finish(false), () => scope.setTimeout(() => finish(false), waitMs));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Runs inside the sandbox frame. Serialized with `toString()`, so it must
|
|
143
|
+
* stay self-contained. Starts the worker from the source embedded in the
|
|
144
|
+
* document and relays messages between the page and the worker, one at a
|
|
145
|
+
* time: the next message goes to the worker only after it answered the last.
|
|
146
|
+
* The worker answers only once the work the program queued is done (see
|
|
147
|
+
* `bootWorker`). The watchdog times only the message being processed. A
|
|
148
|
+
* message that takes longer than `timeoutMs` gets the worker restarted, and the messages
|
|
149
|
+
* queued behind it are dropped, since they were meant for the old state.
|
|
150
|
+
* Messages from anything but the parent window are ignored. Nothing starts
|
|
151
|
+
* until `checkPolicy` confirms the frame's own policy is in force; without
|
|
152
|
+
* it the frame tells the page `unprotected` and stays inert.
|
|
153
|
+
*
|
|
154
|
+
* Once the worker has had a message, every `heartbeatMs` it sits idle the
|
|
155
|
+
* runner sends it a ping under the same watchdog, and keeps the answer from
|
|
156
|
+
* the page. A worker spinning between messages is then restarted, and the
|
|
157
|
+
* page told, as if a message had hung. No pings go out while the page is
|
|
158
|
+
* hidden.
|
|
159
|
+
*/
|
|
160
|
+
function bootRunner(workerSource, timeoutMs, heartbeatMs, checkPolicy, policy, policyWaitMs) {
|
|
161
|
+
const workerUrl = URL.createObjectURL(new Blob([workerSource], { type: "text/javascript" }));
|
|
162
|
+
let worker = null;
|
|
163
|
+
let queue = [];
|
|
164
|
+
let busy = null;
|
|
165
|
+
/** Whether the worker has had a message, so it may have work running. */
|
|
166
|
+
let used = false;
|
|
167
|
+
let heartbeat = null;
|
|
168
|
+
const ping = {
|
|
169
|
+
seq: -1,
|
|
170
|
+
type: "ping"
|
|
171
|
+
};
|
|
172
|
+
const toPage = (message) => {
|
|
173
|
+
window.parent.postMessage(message, "*");
|
|
174
|
+
};
|
|
175
|
+
const beat = () => {
|
|
176
|
+
heartbeat = null;
|
|
177
|
+
if (busy === null && queue.length === 0 && !document.hidden) {
|
|
178
|
+
queue.push(ping);
|
|
179
|
+
next();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** Schedules the next ping, if the worker is idle and has run something. */
|
|
183
|
+
const rest = () => {
|
|
184
|
+
if (heartbeat === null && used && busy === null && queue.length === 0 && !document.hidden) heartbeat = window.setTimeout(beat, heartbeatMs);
|
|
185
|
+
};
|
|
186
|
+
const next = () => {
|
|
187
|
+
const message = queue.shift();
|
|
188
|
+
if (busy !== null || worker === null || message === void 0) {
|
|
189
|
+
if (message !== void 0) queue.unshift(message);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const timer = window.setTimeout(() => {
|
|
193
|
+
worker?.terminate();
|
|
194
|
+
queue = [];
|
|
195
|
+
busy = null;
|
|
196
|
+
start();
|
|
197
|
+
toPage({
|
|
198
|
+
seq: message.seq,
|
|
199
|
+
type: "quarantined"
|
|
200
|
+
});
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
busy = {
|
|
203
|
+
seq: message.seq,
|
|
204
|
+
timer
|
|
205
|
+
};
|
|
206
|
+
used = true;
|
|
207
|
+
worker.postMessage(message);
|
|
208
|
+
};
|
|
209
|
+
const start = () => {
|
|
210
|
+
const created = new Worker(workerUrl);
|
|
211
|
+
used = false;
|
|
212
|
+
created.addEventListener("message", (event) => {
|
|
213
|
+
if (busy !== null && event.data.seq === busy.seq) {
|
|
214
|
+
window.clearTimeout(busy.timer);
|
|
215
|
+
busy = null;
|
|
216
|
+
}
|
|
217
|
+
if (event.data.seq !== ping.seq) toPage(event.data);
|
|
218
|
+
next();
|
|
219
|
+
rest();
|
|
220
|
+
});
|
|
221
|
+
worker = created;
|
|
222
|
+
};
|
|
223
|
+
const boot = () => {
|
|
224
|
+
window.addEventListener("message", (event) => {
|
|
225
|
+
if (event.source !== window.parent) return;
|
|
226
|
+
queue.push(event.data);
|
|
227
|
+
next();
|
|
228
|
+
});
|
|
229
|
+
document.addEventListener("visibilitychange", rest);
|
|
230
|
+
start();
|
|
231
|
+
toPage({ type: "ready" });
|
|
232
|
+
};
|
|
233
|
+
checkPolicy({
|
|
234
|
+
addEventListener: (type, listener) => {
|
|
235
|
+
document.addEventListener(type, listener);
|
|
236
|
+
},
|
|
237
|
+
fetch: (url) => fetch(url),
|
|
238
|
+
removeEventListener: (type, listener) => {
|
|
239
|
+
document.removeEventListener(type, listener);
|
|
240
|
+
},
|
|
241
|
+
setTimeout: (callback, ms) => window.setTimeout(callback, ms)
|
|
242
|
+
}, policy, policyWaitMs).then((inForce) => {
|
|
243
|
+
if (inForce) boot();
|
|
244
|
+
else toPage({ type: "unprotected" });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** Source of the sandbox worker: the runtime, booted with network and code loading removed. */
|
|
248
|
+
const workerSource = `(${bootWorker.toString()})(${createRuntime.toString()}, ${JSON.stringify(LOCKED_GLOBALS)});`;
|
|
249
|
+
/** Embeds a script in HTML without letting it close the `<script>` element. */
|
|
250
|
+
const scriptText = (code) => code.replaceAll("</", String.raw`<\/`);
|
|
251
|
+
/** The sandbox frame document the server sends on `GET`. */
|
|
252
|
+
function frameDocument() {
|
|
253
|
+
return `<!doctype html>
|
|
254
|
+
<meta charset="utf-8">
|
|
255
|
+
<script>${scriptText(`(${bootRunner.toString()})(${JSON.stringify(workerSource)}, ${WATCHDOG_MS}, ${HEARTBEAT_MS}, ${framePolicyInForce.toString()}, ${JSON.stringify(frameSecurityPolicy)}, ${POLICY_WAIT_MS});`)}<\/script>`;
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
export { HEARTBEAT_MS, WATCHDOG_MS, createRuntime, frameDocument, framePolicyInForce, frameSecurityPolicy, workerSource };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { frameSecurityPolicy } from "./frame-policy.js";
|
|
2
|
+
import { ChatBlock, ChatBlockStatus, ChildrenKind, ComponentSchema, Diagnostic, DiagnosticFix, Invalidation, JsonValue, Message, NumberPropSchema, PROGRAM_FORMAT, PickleErrorCode, PickleErrorData, ProcedureEntry, ProcedureKind, Program, PropSchema, SessionBody, SessionEvent, SessionOptions, Span, pickleErrorCodes } from "./types.js";
|
|
3
|
+
import { FrameMessage, HostMessage, MutationCall, MutationResult, Op, QueryRead, QueryResult, ROOT_ID, Row, RuntimeError, WireArg, WireValue, WorkerMessage } from "./protocol.js";
|
|
4
|
+
import { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent } from "./parse.js";
|
|
5
|
+
import { MAX_NODES, MAX_OPS, MAX_TEXT, TreeNode, TreeStore, createTreeStore } from "./tree-store.js";
|
|
6
|
+
import { isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost } from "./urls.js";
|
|
7
|
+
export { type ChatBlock, type ChatBlockStatus, type ChildrenKind, type ComponentSchema, type Diagnostic, type DiagnosticFix, type FrameMessage, type HostMessage, type Invalidation, type JsonValue, MAX_NODES, MAX_OPS, MAX_TEXT, type Message, type MutationCall, type MutationResult, type NumberPropSchema, type Op, PROGRAM_FORMAT, type PickleErrorCode, type PickleErrorData, type ProcedureEntry, type ProcedureKind, type Program, type PropSchema, type QueryRead, type QueryResult, ROOT_ID, type Row, type RuntimeError, type SessionBody, type SessionEvent, type SessionOptions, type Span, type TreeNode, type TreeStore, type WireArg, type WireValue, type WorkerMessage, createTreeStore, frameSecurityPolicy, isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost, parseErrorData, parseInvalidations, parseProgram, parseSessionEvent, pickleErrorCodes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PROGRAM_FORMAT, pickleErrorCodes } from "./types.js";
|
|
2
|
+
import { ROOT_ID } from "./protocol.js";
|
|
3
|
+
import { frameSecurityPolicy } from "./frame-policy.js";
|
|
4
|
+
import { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent } from "./parse.js";
|
|
5
|
+
import { isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost } from "./urls.js";
|
|
6
|
+
import { MAX_NODES, MAX_OPS, MAX_TEXT, createTreeStore } from "./tree-store.js";
|
|
7
|
+
export { MAX_NODES, MAX_OPS, MAX_TEXT, PROGRAM_FORMAT, ROOT_ID, createTreeStore, frameSecurityPolicy, isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost, parseErrorData, parseInvalidations, parseProgram, parseSessionEvent, pickleErrorCodes };
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Invalidation, PickleErrorData, Program, SessionEvent } from "./types.js";
|
|
2
|
+
//#region src/parse.d.ts
|
|
3
|
+
export declare function parseErrorData(value: unknown): PickleErrorData | null;
|
|
4
|
+
/** The `invalidate` list of a mutation's reply. */
|
|
5
|
+
export declare function parseInvalidations(value: unknown): ReadonlyArray<Invalidation> | null;
|
|
6
|
+
export declare function parseProgram(value: unknown): Program | null;
|
|
7
|
+
/** One session event from its JSON text, or null when it is not one. */
|
|
8
|
+
export declare function parseSessionEvent(text: string): SessionEvent | null;
|
|
9
|
+
//#endregion
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { pickleErrorCodes } from "./types.js";
|
|
2
|
+
//#region src/parse.ts
|
|
3
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
const isString = (value) => typeof value === "string";
|
|
5
|
+
const isStringList = (value) => Array.isArray(value) && value.every(isString);
|
|
6
|
+
function recordOf(value, item) {
|
|
7
|
+
if (!isRecord(value)) return null;
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10
|
+
const parsed = item(entry);
|
|
11
|
+
if (parsed === null) return null;
|
|
12
|
+
out[key] = parsed;
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
const isErrorCode = (value) => pickleErrorCodes.some((code) => code === value);
|
|
17
|
+
function parseErrorData(value) {
|
|
18
|
+
if (!isRecord(value) || !isErrorCode(value.code) || !isString(value.message)) return null;
|
|
19
|
+
return {
|
|
20
|
+
code: value.code,
|
|
21
|
+
message: value.message
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function parseInvalidation(value) {
|
|
25
|
+
if (!isRecord(value) || !isString(value.name)) return null;
|
|
26
|
+
if (!("input" in value)) return { name: value.name };
|
|
27
|
+
return {
|
|
28
|
+
input: value.input,
|
|
29
|
+
name: value.name
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** The `invalidate` list of a mutation's reply. */
|
|
33
|
+
function parseInvalidations(value) {
|
|
34
|
+
if (!Array.isArray(value)) return null;
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const entry of value) {
|
|
37
|
+
const parsed = parseInvalidation(entry);
|
|
38
|
+
if (parsed === null) return null;
|
|
39
|
+
out.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function parsePropSchema(value) {
|
|
44
|
+
if (!isRecord(value)) return null;
|
|
45
|
+
switch (value.kind) {
|
|
46
|
+
case "boolean":
|
|
47
|
+
case "handler":
|
|
48
|
+
case "image":
|
|
49
|
+
case "link":
|
|
50
|
+
case "list":
|
|
51
|
+
case "string": return { kind: value.kind };
|
|
52
|
+
case "enum": return isStringList(value.values) ? {
|
|
53
|
+
kind: "enum",
|
|
54
|
+
values: value.values
|
|
55
|
+
} : null;
|
|
56
|
+
case "number": {
|
|
57
|
+
const schema = { kind: "number" };
|
|
58
|
+
if (typeof value.min === "number") schema.min = value.min;
|
|
59
|
+
if (typeof value.max === "number") schema.max = value.max;
|
|
60
|
+
return schema;
|
|
61
|
+
}
|
|
62
|
+
default: return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const isChildrenKind = (value) => value === "block" || value === "inline" || value === "none";
|
|
66
|
+
function parseComponentSchema(value) {
|
|
67
|
+
if (!isRecord(value) || !isChildrenKind(value.children)) return null;
|
|
68
|
+
const props = recordOf(value.props, parsePropSchema);
|
|
69
|
+
return props === null ? null : {
|
|
70
|
+
children: value.children,
|
|
71
|
+
props
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function parseProcedure(value) {
|
|
75
|
+
if (!isRecord(value) || value.kind !== "query" && value.kind !== "mutation" && value.kind !== "action" || !isStringList(value.invalidates)) return null;
|
|
76
|
+
return {
|
|
77
|
+
invalidates: value.invalidates,
|
|
78
|
+
kind: value.kind
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseProgram(value) {
|
|
82
|
+
if (!isRecord(value) || value.format !== 1 || !isString(value.code) || !isStringList(value.imageHosts)) return null;
|
|
83
|
+
const components = recordOf(value.components, parseComponentSchema);
|
|
84
|
+
const procedures = recordOf(value.procedures, parseProcedure);
|
|
85
|
+
const prose = recordOf(value.prose, (entry) => isString(entry) ? entry : null);
|
|
86
|
+
if (components === null || procedures === null || prose === null) return null;
|
|
87
|
+
return {
|
|
88
|
+
code: value.code,
|
|
89
|
+
components,
|
|
90
|
+
format: 1,
|
|
91
|
+
imageHosts: value.imageHosts,
|
|
92
|
+
procedures,
|
|
93
|
+
prose
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseDiagnostic(value) {
|
|
97
|
+
if (!isRecord(value) || !isString(value.code) || !isString(value.message) || value.severity !== "error" && value.severity !== "warning" && value.severity !== "info" && value.severity !== "hint" || !isRecord(value.span) || typeof value.span.line !== "number") return null;
|
|
98
|
+
return {
|
|
99
|
+
code: value.code,
|
|
100
|
+
message: value.message,
|
|
101
|
+
severity: value.severity,
|
|
102
|
+
span: { line: value.span.line }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The diagnostics that parse; one the browser cannot read is skipped, not the event. */
|
|
106
|
+
function parseDiagnostics(value) {
|
|
107
|
+
if (!Array.isArray(value)) return null;
|
|
108
|
+
return value.flatMap((entry) => {
|
|
109
|
+
const parsed = parseDiagnostic(entry);
|
|
110
|
+
return parsed === null ? [] : [parsed];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** One session event from its JSON text, or null when it is not one. */
|
|
114
|
+
function parseSessionEvent(text) {
|
|
115
|
+
let value;
|
|
116
|
+
try {
|
|
117
|
+
value = JSON.parse(text);
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (!isRecord(value)) return null;
|
|
122
|
+
if (value.type === "error") {
|
|
123
|
+
const error = parseErrorData(value.error);
|
|
124
|
+
return error === null ? null : {
|
|
125
|
+
error,
|
|
126
|
+
type: "error"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const diagnostics = parseDiagnostics(value.diagnostics);
|
|
130
|
+
if (diagnostics === null || typeof value.revision !== "number") return null;
|
|
131
|
+
if (value.type === "program") {
|
|
132
|
+
const program = parseProgram(value.program);
|
|
133
|
+
return program === null ? null : {
|
|
134
|
+
diagnostics,
|
|
135
|
+
fresh: value.fresh === true,
|
|
136
|
+
program,
|
|
137
|
+
revision: value.revision,
|
|
138
|
+
type: "program"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (value.type === "done" && isString(value.source)) return {
|
|
142
|
+
diagnostics,
|
|
143
|
+
revision: value.revision,
|
|
144
|
+
source: value.source,
|
|
145
|
+
type: "done"
|
|
146
|
+
};
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent };
|