@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 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
- # Temporary Holding Version
1
+ # @jigs.design/runtime
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 };
@@ -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 };
@@ -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 };
@@ -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 };