@jigs.design/react 0.1.0 → 0.1.2
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/README.md +20 -19
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -3
- package/dist/view/{PickleView.d.ts → JigsView.d.ts} +4 -5
- package/dist/view/{PickleView.js → JigsView.js} +9 -28
- package/dist/view/local-value.d.ts +19 -0
- package/dist/view/local-value.js +47 -0
- package/dist/view/session.d.ts +4 -4
- package/dist/view/settled.d.ts +1 -1
- package/dist/view/settled.js +1 -1
- package/dist/view/{usePickleSession.d.ts → useJigsSession.d.ts} +5 -5
- package/dist/view/{usePickleSession.js → useJigsSession.js} +3 -3
- package/package.json +5 -5
- package/dist/view/controller.d.ts +0 -22
- package/dist/view/controller.js +0 -524
- package/dist/view/frame-check.js +0 -32
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# @jigs.design/react
|
|
2
2
|
|
|
3
|
-
The web renderer. `
|
|
3
|
+
The web renderer. `JigsView` runs a compiled program in a sandboxed frame and draws it with your registry. `useJigsSession` holds a conversation with the server and streams its latest program.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
import { components } from "@jigs.design/catalog/react";
|
|
7
|
-
import {
|
|
8
|
-
import { client } from "./
|
|
7
|
+
import { JigsView, useJigsSession } from "@jigs.design/react";
|
|
8
|
+
import { client } from "./jigs-client";
|
|
9
9
|
|
|
10
10
|
export function App() {
|
|
11
|
-
const session =
|
|
11
|
+
const session = useJigsSession(client);
|
|
12
12
|
return (
|
|
13
13
|
<>
|
|
14
14
|
<button onClick={() => session.send("My open orders")}>Ask</button>
|
|
15
15
|
{session.program && (
|
|
16
|
-
<
|
|
16
|
+
<JigsView
|
|
17
17
|
client={client}
|
|
18
18
|
components={components}
|
|
19
19
|
documentId={session.documentId}
|
|
@@ -25,28 +25,29 @@ export function App() {
|
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
The program runs in a worker inside a frame served by your
|
|
28
|
+
The program runs in a worker inside a frame served by your Jigs route. The frame has no network: queries and mutations go through the client, and every prop is checked against the program's schema before it reaches your component. A new `program` replaces the running one and keeps the state it still declares. `onAction` handles the program's actions after the server checked their input. The view drops an `autoFocus` prop unless you pass `allowAutoFocus`, so a jig streaming into a chat cannot take the focus. It checks what the worker asks for against the program, since the worker is not trusted.
|
|
29
29
|
|
|
30
30
|
## Exports
|
|
31
31
|
|
|
32
|
-
| Export
|
|
33
|
-
|
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `useHandlerSettled`
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
32
|
+
| Export | What it is |
|
|
33
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
34
|
+
| `JigsView` | Runs one program and draws it. Takes `client`, `components`, `program`, `onAction`, `showIssues`, `allowAutoFocus` |
|
|
35
|
+
| `useJigsSession` | The conversation and its latest program. `initialMessages` resumes one |
|
|
36
|
+
| `useHandlerSettled` | For a registry control: changes once the program has answered its events, so it can drop an edit the program refused |
|
|
37
|
+
| `useLocalValue`, `controlled` | For a controlled control: a local copy of the program's value that runs ahead of the round trip and drops a refused edit |
|
|
38
|
+
| `Registry`, `RegistryFor` | The type of a map from catalog names to React components |
|
|
39
|
+
| `SessionState`, `JigsSession` | The session's state and the hook's return value |
|
|
40
|
+
| `ViewIssue` | What `onIssue` receives |
|
|
39
41
|
|
|
40
|
-
It re-exports the shared types from `@jigs.design/runtime`.
|
|
42
|
+
It re-exports the shared types from `@jigs.design/runtime`. The frame, its messages and the calls through the client are `createViewController` in `@jigs.design/client`, which `JigsView` is built on; a renderer for another framework starts there.
|
|
41
43
|
|
|
42
44
|
## Source
|
|
43
45
|
|
|
44
|
-
- `src/view/
|
|
45
|
-
- `src/view/controller.ts`: the frame, its messages, and the calls it makes through the client.
|
|
46
|
-
- `src/view/frame-check.ts`: tells you when a page policy broke the frame's own.
|
|
46
|
+
- `src/view/JigsView.tsx`: the view component, on the client's view controller.
|
|
47
47
|
- `src/view/registry.ts`, `src/view/settled.ts`: the registry types and `useHandlerSettled`.
|
|
48
|
-
- `src/view/
|
|
48
|
+
- `src/view/local-value.ts`: `useLocalValue` and `controlled`, for controls.
|
|
49
|
+
- `src/view/session.ts`, `src/view/useJigsSession.ts`: the session store and its hook.
|
|
49
50
|
|
|
50
51
|
## Docs
|
|
51
52
|
|
|
52
|
-
[React](../../apps/docs/docs/react/index.mdx), [Registry](../../apps/docs/docs/react/registry.mdx), [Sandbox](../../apps/docs/docs/react/sandbox.mdx), [Session](../../apps/docs/docs/running/session.mdx).
|
|
53
|
+
[React](../../apps/docs/docs/react/index.mdx), [Registry](../../apps/docs/docs/react/registry.mdx), [Sandbox](../../apps/docs/docs/react/sandbox.mdx), [Session](../../apps/docs/docs/running/session.mdx), [Renderers](../../apps/docs/docs/renderers/index.mdx).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { controlled, useLocalValue } from "./view/local-value.js";
|
|
2
2
|
import { Registry, RegistryFor } from "./view/registry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { JigsView } from "./view/JigsView.js";
|
|
4
4
|
import { useHandlerSettled } from "./view/settled.js";
|
|
5
5
|
import { SessionState } from "./view/session.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
export { type ConfirmFunction, type Diagnostic, type
|
|
6
|
+
import { JigsSession, useJigsSession } from "./view/useJigsSession.js";
|
|
7
|
+
import { ConfirmFunction, ViewIssue } from "@jigs.design/client";
|
|
8
|
+
import { Diagnostic, JigsErrorCode, JigsErrorData, Message, Program, SessionEvent, SessionOptions, Span } from "@jigs.design/runtime";
|
|
9
|
+
export { type ConfirmFunction, type Diagnostic, type JigsErrorCode, type JigsErrorData, type JigsSession, JigsView, type Message, type Program, type Registry, type RegistryFor, type SessionEvent, type SessionOptions, type SessionState, type Span, type ViewIssue, controlled, useHandlerSettled, useJigsSession, useLocalValue };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useHandlerSettled } from "./view/settled.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { controlled, useLocalValue } from "./view/local-value.js";
|
|
3
|
+
import { JigsView } from "./view/JigsView.js";
|
|
4
|
+
import { useJigsSession } from "./view/useJigsSession.js";
|
|
5
|
+
export { JigsView, controlled, useHandlerSettled, useJigsSession, useLocalValue };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ViewIssue } from "./controller.js";
|
|
2
1
|
import { Registry } from "./registry.js";
|
|
2
|
+
import { ConfirmFunction, JigsClient, ViewIssue } from "@jigs.design/client";
|
|
3
3
|
import { Program } from "@jigs.design/runtime";
|
|
4
|
-
|
|
5
|
-
//#region src/view/PickleView.d.ts
|
|
4
|
+
//#region src/view/JigsView.d.ts
|
|
6
5
|
/** An action as `onAction` receives it; type it with `ActionEvent<typeof host>`. */
|
|
7
6
|
interface ActionLike {
|
|
8
7
|
input: unknown;
|
|
@@ -14,14 +13,14 @@ interface ActionLike {
|
|
|
14
13
|
* keeps its value, unless `documentId` changed with it. A new `client`
|
|
15
14
|
* starts the program over on that client.
|
|
16
15
|
*/
|
|
17
|
-
export declare function
|
|
16
|
+
export declare function JigsView<TAction extends ActionLike = ActionLike>(props: {
|
|
18
17
|
/**
|
|
19
18
|
* Let the program focus a control when it appears, through the
|
|
20
19
|
* `autoFocus` prop. Default false: in a chat, a UI that streams in would
|
|
21
20
|
* otherwise take the focus from the composer while the user types.
|
|
22
21
|
*/
|
|
23
22
|
allowAutoFocus?: boolean;
|
|
24
|
-
client:
|
|
23
|
+
client: JigsClient;
|
|
25
24
|
components: Registry;
|
|
26
25
|
/**
|
|
27
26
|
* Which UI `program` is a version of. When it changes, the program starts
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { createViewController } from "./controller.js";
|
|
2
1
|
import { SettledContext } from "./settled.js";
|
|
3
|
-
import { ROOT_ID } from "@jigs.design/runtime";
|
|
4
2
|
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { createViewController, missingComponents } from "@jigs.design/client";
|
|
4
|
+
import { ROOT_ID } from "@jigs.design/runtime";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
//#region src/view/
|
|
7
|
-
/** The prop a registry component reads to focus itself when it mounts. */
|
|
8
|
-
const AUTO_FOCUS = "autoFocus";
|
|
9
|
-
const toWireArgs = (args) => args.filter((arg) => typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean");
|
|
6
|
+
//#region src/view/JigsView.tsx
|
|
10
7
|
function TreeNodeView(props) {
|
|
11
8
|
const { components, controller } = props;
|
|
12
9
|
const getNode = () => controller.store.get(props.id);
|
|
@@ -24,21 +21,7 @@ function TreeNodeView(props) {
|
|
|
24
21
|
if (node.type === "#root") return children;
|
|
25
22
|
const registered = Object.hasOwn(components, node.type) ? components[node.type] : void 0;
|
|
26
23
|
if (registered === void 0) return null;
|
|
27
|
-
const drawn = {};
|
|
28
|
-
for (const [name, value] of Object.entries(node.props)) {
|
|
29
|
-
if (name === AUTO_FOCUS && !props.allowAutoFocus) continue;
|
|
30
|
-
const kind = controller.propKind(node.type, name);
|
|
31
|
-
if (kind === "handler") {
|
|
32
|
-
if (typeof value === "object" && "fn" in value) {
|
|
33
|
-
const fn = value.fn;
|
|
34
|
-
drawn[name] = (...args) => {
|
|
35
|
-
controller.call(node.id, fn, toWireArgs(args));
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
} else if (kind === "list") {
|
|
39
|
-
if (typeof value === "object" && "rows" in value) drawn[name] = value.rows;
|
|
40
|
-
} else if (kind !== void 0 && typeof value !== "object") drawn[name] = value;
|
|
41
|
-
}
|
|
24
|
+
const drawn = controller.drawnProps(node, { allowAutoFocus: props.allowAutoFocus });
|
|
42
25
|
return /* @__PURE__ */ jsx(SettledContext, {
|
|
43
26
|
value: settled,
|
|
44
27
|
children: /* @__PURE__ */ jsx(registered, {
|
|
@@ -47,16 +30,14 @@ function TreeNodeView(props) {
|
|
|
47
30
|
})
|
|
48
31
|
});
|
|
49
32
|
}
|
|
50
|
-
|
|
51
|
-
return Object.keys(program.components).filter((name) => !Object.hasOwn(components, name)).map((name) => `<${name}> is not in the registry, so it renders nothing`);
|
|
52
|
-
}
|
|
33
|
+
const missingFromRegistry = (program, components) => missingComponents(program, Object.keys(components));
|
|
53
34
|
/**
|
|
54
35
|
* Runs one program in the sandbox and draws it with `components`. A new
|
|
55
36
|
* `program` replaces the running one; state the new program still declares
|
|
56
37
|
* keeps its value, unless `documentId` changed with it. A new `client`
|
|
57
38
|
* starts the program over on that client.
|
|
58
39
|
*/
|
|
59
|
-
function
|
|
40
|
+
function JigsView(props) {
|
|
60
41
|
const { program } = props;
|
|
61
42
|
const [view, setView] = useState(() => ({
|
|
62
43
|
client: props.client,
|
|
@@ -111,15 +92,15 @@ function PickleView(props) {
|
|
|
111
92
|
id: ROOT_ID
|
|
112
93
|
}),
|
|
113
94
|
showIssues && status.notice !== null && /* @__PURE__ */ jsx("p", {
|
|
114
|
-
className: "
|
|
95
|
+
className: "jigs-view-notice",
|
|
115
96
|
role: "status",
|
|
116
97
|
children: status.notice
|
|
117
98
|
}),
|
|
118
99
|
errors.length + issues.length > 0 && /* @__PURE__ */ jsxs("ul", {
|
|
119
|
-
className: "
|
|
100
|
+
className: "jigs-view-errors",
|
|
120
101
|
children: [errors.map((error, index) => /* @__PURE__ */ jsxs("li", { children: [error.line === null ? "" : `Line ${error.line}: `, error.message] }, `error:${index}:${error.message}`)), issues.map((issue) => /* @__PURE__ */ jsx("li", { children: issue }, `issue:${issue}`))]
|
|
121
102
|
})
|
|
122
103
|
] });
|
|
123
104
|
}
|
|
124
105
|
//#endregion
|
|
125
|
-
export {
|
|
106
|
+
export { JigsView };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/view/local-value.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether the program controls a control's value: it sets the value prop
|
|
4
|
+
* (`value`, or `checked`) and handles `onChange`. A control with only
|
|
5
|
+
* `onChange` is uncontrolled, like a React input without `value`: the
|
|
6
|
+
* program hears each edit but the control keeps what the user did.
|
|
7
|
+
*/
|
|
8
|
+
export declare const controlled: <T>(value: T | undefined, onChange: ((next: T) => void) | undefined) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A local copy of a value the program owns, so a control can run ahead of the
|
|
11
|
+
* round trip to the sandbox. Pass `echoes` when the program controls the
|
|
12
|
+
* value (`controlled`): echoes of the edits sent to it then don't reset the
|
|
13
|
+
* control, but any other new value does, and so does an edit the program did
|
|
14
|
+
* not take: once it has answered every call (`useHandlerSettled`) and `value`
|
|
15
|
+
* is still not the edit, the control goes back to `value`. Without `echoes`,
|
|
16
|
+
* the control keeps its own state until `value` changes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useLocalValue<T>(value: T, echoes: boolean): [T, (next: T) => void];
|
|
19
|
+
//#endregion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useHandlerSettled } from "./settled.js";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
//#region src/view/local-value.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether the program controls a control's value: it sets the value prop
|
|
6
|
+
* (`value`, or `checked`) and handles `onChange`. A control with only
|
|
7
|
+
* `onChange` is uncontrolled, like a React input without `value`: the
|
|
8
|
+
* program hears each edit but the control keeps what the user did.
|
|
9
|
+
*/
|
|
10
|
+
const controlled = (value, onChange) => value !== void 0 && onChange !== void 0;
|
|
11
|
+
/**
|
|
12
|
+
* A local copy of a value the program owns, so a control can run ahead of the
|
|
13
|
+
* round trip to the sandbox. Pass `echoes` when the program controls the
|
|
14
|
+
* value (`controlled`): echoes of the edits sent to it then don't reset the
|
|
15
|
+
* control, but any other new value does, and so does an edit the program did
|
|
16
|
+
* not take: once it has answered every call (`useHandlerSettled`) and `value`
|
|
17
|
+
* is still not the edit, the control goes back to `value`. Without `echoes`,
|
|
18
|
+
* the control keeps its own state until `value` changes.
|
|
19
|
+
*/
|
|
20
|
+
function useLocalValue(value, echoes) {
|
|
21
|
+
const settled = useHandlerSettled();
|
|
22
|
+
const [local, setLocal] = useState(value);
|
|
23
|
+
const [seen, setSeen] = useState(value);
|
|
24
|
+
const [seenSettled, setSeenSettled] = useState(settled);
|
|
25
|
+
const [pending, setPending] = useState([]);
|
|
26
|
+
if (!Object.is(value, seen)) {
|
|
27
|
+
setSeen(value);
|
|
28
|
+
const echo = pending.findIndex((sent) => Object.is(sent, value));
|
|
29
|
+
if (echo === -1) {
|
|
30
|
+
setLocal(value);
|
|
31
|
+
setPending([]);
|
|
32
|
+
} else setPending(pending.slice(echo + 1));
|
|
33
|
+
} else if (settled !== seenSettled) {
|
|
34
|
+
setSeenSettled(settled);
|
|
35
|
+
if (pending.length > 0) {
|
|
36
|
+
setLocal(value);
|
|
37
|
+
setPending([]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const edit = (next) => {
|
|
41
|
+
setLocal(next);
|
|
42
|
+
if (echoes) setPending([...pending.slice(-20), next]);
|
|
43
|
+
};
|
|
44
|
+
return [local, edit];
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { controlled, useLocalValue };
|
package/dist/view/session.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { JigsClient } from "@jigs.design/client";
|
|
2
|
+
import { Diagnostic, JigsErrorData, Message, Program, SessionOptions } from "@jigs.design/runtime";
|
|
3
3
|
//#region src/view/session.d.ts
|
|
4
4
|
export interface SessionState {
|
|
5
5
|
diagnostics: ReadonlyArray<Diagnostic>;
|
|
@@ -7,11 +7,11 @@ export interface SessionState {
|
|
|
7
7
|
* Which UI `program` is a version of. It goes up when the model writes a
|
|
8
8
|
* new UI rather than editing the last one, or a stored one is restored,
|
|
9
9
|
* and goes back with the program when a turn is stopped. Pass it to
|
|
10
|
-
* `
|
|
10
|
+
* `JigsView` as `documentId`, so a new UI starts from fresh state.
|
|
11
11
|
*/
|
|
12
12
|
documentId: number;
|
|
13
13
|
/** `{ code, message }` when `status` is "error". */
|
|
14
|
-
error:
|
|
14
|
+
error: JigsErrorData | null;
|
|
15
15
|
/** The conversation, to store or render. Assistant entries hold checked source. */
|
|
16
16
|
messages: ReadonlyArray<Message>;
|
|
17
17
|
/** The latest program, or null before the first one arrives. */
|
package/dist/view/settled.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* component's latest events and its answer is on screen. A control that
|
|
6
6
|
* runs ahead of the program, such as a text field showing what was typed,
|
|
7
7
|
* can then go back to its value prop if the program kept another value.
|
|
8
|
-
* Always 0 outside `
|
|
8
|
+
* Always 0 outside `JigsView`.
|
|
9
9
|
*/
|
|
10
10
|
export declare function useHandlerSettled(): number;
|
|
11
11
|
//#endregion
|
package/dist/view/settled.js
CHANGED
|
@@ -8,7 +8,7 @@ const SettledContext = createContext(0);
|
|
|
8
8
|
* component's latest events and its answer is on screen. A control that
|
|
9
9
|
* runs ahead of the program, such as a text field showing what was typed,
|
|
10
10
|
* can then go back to its value prop if the program kept another value.
|
|
11
|
-
* Always 0 outside `
|
|
11
|
+
* Always 0 outside `JigsView`.
|
|
12
12
|
*/
|
|
13
13
|
function useHandlerSettled() {
|
|
14
14
|
return useContext(SettledContext);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SessionState } from "./session.js";
|
|
2
|
+
import { JigsClient } from "@jigs.design/client";
|
|
2
3
|
import { Message, SessionOptions } from "@jigs.design/runtime";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export interface PickleSession<TOptions extends SessionOptions = SessionOptions> extends SessionState {
|
|
4
|
+
//#region src/view/useJigsSession.d.ts
|
|
5
|
+
export interface JigsSession<TOptions extends SessionOptions = SessionOptions> extends SessionState {
|
|
6
6
|
/**
|
|
7
7
|
* Stops any turn and starts over with `messages` (default none). Like
|
|
8
8
|
* `initialMessages`, messages that end with an assistant message are
|
|
@@ -25,7 +25,7 @@ export interface PickleSession<TOptions extends SessionOptions = SessionOptions>
|
|
|
25
25
|
* conversation later, call `reset(messages)`. A new `client` stops the turn
|
|
26
26
|
* in flight and compiles the conversation's last answer again through it.
|
|
27
27
|
*/
|
|
28
|
-
export declare function
|
|
28
|
+
export declare function useJigsSession<TOptions extends SessionOptions = SessionOptions>(client: JigsClient, options?: {
|
|
29
29
|
initialMessages?: ReadonlyArray<Message>;
|
|
30
|
-
}):
|
|
30
|
+
}): JigsSession<TOptions>;
|
|
31
31
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSessionStore } from "./session.js";
|
|
2
2
|
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
-
//#region src/view/
|
|
3
|
+
//#region src/view/useJigsSession.ts
|
|
4
4
|
const NO_MESSAGES = [];
|
|
5
5
|
/**
|
|
6
6
|
* Holds a conversation with the server and its latest program. Pass
|
|
@@ -10,7 +10,7 @@ const NO_MESSAGES = [];
|
|
|
10
10
|
* conversation later, call `reset(messages)`. A new `client` stops the turn
|
|
11
11
|
* in flight and compiles the conversation's last answer again through it.
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
13
|
+
function useJigsSession(client, options) {
|
|
14
14
|
const [store] = useState(() => createSessionStore(client, options?.initialMessages ?? NO_MESSAGES));
|
|
15
15
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
16
16
|
useEffect(() => {
|
|
@@ -26,4 +26,4 @@ function usePickleSession(client, options) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
|
-
export {
|
|
29
|
+
export { useJigsSession };
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/react",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "The web renderer:
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "The web renderer: JigsView runs a compiled program in a sandbox and draws it with your registry; useJigsSession streams programs from the server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/yoamomonstruos/
|
|
8
|
+
"url": "git+https://github.com/yoamomonstruos/jigs.git",
|
|
9
9
|
"directory": "packages/react"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@jigs.design/
|
|
29
|
-
"@jigs.design/
|
|
28
|
+
"@jigs.design/client": "0.1.2",
|
|
29
|
+
"@jigs.design/runtime": "0.1.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Program } from "@jigs.design/runtime";
|
|
2
|
-
import { ConfirmFunction, PickleClient } from "@jigs.design/client";
|
|
3
|
-
//#region src/view/controller.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* A problem the view found, as `onIssue` receives it: a runtime error the
|
|
6
|
-
* program threw, something the view dropped (a prop or component that failed
|
|
7
|
-
* its schema, a request the program does not declare, a component missing
|
|
8
|
-
* from the registry), or a notice that the sandbox rejected or stopped a
|
|
9
|
-
* program or cannot run.
|
|
10
|
-
*/
|
|
11
|
-
export type ViewIssue = {
|
|
12
|
-
kind: "dropped";
|
|
13
|
-
message: string;
|
|
14
|
-
} | {
|
|
15
|
-
kind: "error";
|
|
16
|
-
line: number | null;
|
|
17
|
-
message: string;
|
|
18
|
-
} | {
|
|
19
|
-
kind: "notice";
|
|
20
|
-
message: string;
|
|
21
|
-
};
|
|
22
|
-
//#endregion
|
package/dist/view/controller.js
DELETED
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
import { frameProblem, unprotectedProblem } from "./frame-check.js";
|
|
2
|
-
import { createTreeStore, parseErrorData, parseProgram } from "@jigs.design/runtime";
|
|
3
|
-
//#region src/view/controller.ts
|
|
4
|
-
const WATCHDOG_NOTICE = "Program stopped: it did not answer within a second. The sandbox was restarted.";
|
|
5
|
-
const GAVE_UP_NOTICE = "Program stopped: it did not answer within a second, again after a restart. It will not run until a new program is loaded.";
|
|
6
|
-
const idle = {
|
|
7
|
-
errors: [],
|
|
8
|
-
issues: [],
|
|
9
|
-
notice: null
|
|
10
|
-
};
|
|
11
|
-
const FORMAT_NOTICE = "This program was compiled for another version of the runtime; compile it again.";
|
|
12
|
-
/**
|
|
13
|
-
* How long after the frame loads it may take to say it is ready. Its policy
|
|
14
|
-
* check takes a moment, or up to a second when the policy is gone, and then
|
|
15
|
-
* the frame says `unprotected` itself; silence past this means its script
|
|
16
|
-
* never ran.
|
|
17
|
-
*/
|
|
18
|
-
const STARTUP_MS = 3e3;
|
|
19
|
-
/**
|
|
20
|
-
* The runtime's own limits, checked again here because the worker shares a
|
|
21
|
-
* realm with the program: different requests a render may read, and
|
|
22
|
-
* mutations one handler call may make.
|
|
23
|
-
*/
|
|
24
|
-
const MAX_QUERIES = 20;
|
|
25
|
-
const MAX_MUTATIONS = 5;
|
|
26
|
-
/** Runtime errors the view keeps from one result, and the length of each. */
|
|
27
|
-
const MAX_ERRORS = 20;
|
|
28
|
-
const MAX_ERROR_TEXT = 1e3;
|
|
29
|
-
/**
|
|
30
|
-
* Times a program may hang before the view stops restarting it. One that
|
|
31
|
-
* hangs on its first render is not tried again; one that hangs later, on
|
|
32
|
-
* data or a handler, gets one restart with fresh state.
|
|
33
|
-
*/
|
|
34
|
-
const MAX_HANGS = 2;
|
|
35
|
-
const NO_ACTIONS = {
|
|
36
|
-
error: {
|
|
37
|
-
code: "UNAVAILABLE",
|
|
38
|
-
message: "This page handles no actions"
|
|
39
|
-
},
|
|
40
|
-
status: "error"
|
|
41
|
-
};
|
|
42
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
-
/** The runtime errors of a result, as text React can draw. The frame is not trusted. */
|
|
44
|
-
function parseErrors(list) {
|
|
45
|
-
const errors = [];
|
|
46
|
-
for (const entry of list) {
|
|
47
|
-
if (errors.length >= MAX_ERRORS) break;
|
|
48
|
-
if (isRecord(entry) && typeof entry.message === "string") {
|
|
49
|
-
const line = entry.line;
|
|
50
|
-
errors.push({
|
|
51
|
-
line: typeof line === "number" && Number.isSafeInteger(line) ? line : null,
|
|
52
|
-
message: entry.message.slice(0, MAX_ERROR_TEXT)
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return errors;
|
|
57
|
-
}
|
|
58
|
-
/** A message from the frame with the fields its kind needs. The frame is not trusted. */
|
|
59
|
-
function isFrameMessage(value) {
|
|
60
|
-
if (typeof value !== "object" || value === null || !("type" in value)) return false;
|
|
61
|
-
if (value.type === "ready" || value.type === "unprotected") return true;
|
|
62
|
-
if (!("seq" in value) || typeof value.seq !== "number") return false;
|
|
63
|
-
if (value.type === "result") return "ops" in value && Array.isArray(value.ops) && "errors" in value && Array.isArray(value.errors) && "queries" in value && Array.isArray(value.queries) && "mutations" in value && Array.isArray(value.mutations);
|
|
64
|
-
if (value.type === "failed") return "message" in value && typeof value.message === "string";
|
|
65
|
-
return value.type === "quarantined";
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Page side of the sandbox. Owns the frame, sends programs and handler calls
|
|
69
|
-
* to the worker, applies the ops it returns to `store`, keeps exactly the
|
|
70
|
-
* queries the program reads subscribed through the client, and runs the
|
|
71
|
-
* mutations its handlers call. Messages from anything but its own frame are
|
|
72
|
-
* ignored, and the names a result asks for must be the right kind of
|
|
73
|
-
* procedure in the program, since the worker shares its realm with the
|
|
74
|
-
* program and is not trusted either.
|
|
75
|
-
*/
|
|
76
|
-
function createViewController(client) {
|
|
77
|
-
const store = createTreeStore();
|
|
78
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
79
|
-
const observers = /* @__PURE__ */ new Map();
|
|
80
|
-
const sent = /* @__PURE__ */ new Map();
|
|
81
|
-
let iframe = null;
|
|
82
|
-
let ready = false;
|
|
83
|
-
let queue = [];
|
|
84
|
-
let seq = 0;
|
|
85
|
-
/** The program the worker accepted and is running. */
|
|
86
|
-
let running = null;
|
|
87
|
-
/** The program the view was last given; the worker may not have accepted it yet. */
|
|
88
|
-
let latest = null;
|
|
89
|
-
let latestKey = null;
|
|
90
|
-
/** The document of the last program sent to the worker, as `load` was told. */
|
|
91
|
-
let latestDocument;
|
|
92
|
-
let lastGood = null;
|
|
93
|
-
const updates = /* @__PURE__ */ new Map();
|
|
94
|
-
/** Times each program hung, against `MAX_HANGS`. */
|
|
95
|
-
let hangs = /* @__PURE__ */ new WeakMap();
|
|
96
|
-
/** Handler calls the worker has not answered yet: seq to node. */
|
|
97
|
-
const calls = /* @__PURE__ */ new Map();
|
|
98
|
-
/** Unanswered calls per node, and how often each node's calls all settled. */
|
|
99
|
-
const openCalls = /* @__PURE__ */ new Map();
|
|
100
|
-
const settled = /* @__PURE__ */ new Map();
|
|
101
|
-
/** What the view itself dropped from results since the last program change. */
|
|
102
|
-
let dropped = [];
|
|
103
|
-
let status = idle;
|
|
104
|
-
/** Bumped on stop and on quarantine, so late results are dropped. */
|
|
105
|
-
let generation = 0;
|
|
106
|
-
let startup = null;
|
|
107
|
-
let actionHandler;
|
|
108
|
-
let confirmHandler;
|
|
109
|
-
/**
|
|
110
|
-
* Aborts the confirmations the program on screen asked for, so a question
|
|
111
|
-
* still showing cannot run a mutation for a program that is gone.
|
|
112
|
-
*/
|
|
113
|
-
let confirmationScope = new AbortController();
|
|
114
|
-
const withdrawConfirmations = () => {
|
|
115
|
-
confirmationScope.abort();
|
|
116
|
-
confirmationScope = new AbortController();
|
|
117
|
-
};
|
|
118
|
-
let issueHandler;
|
|
119
|
-
/** Issues already reported that are still present, by key. */
|
|
120
|
-
let announced = /* @__PURE__ */ new Set();
|
|
121
|
-
let unregistered = [];
|
|
122
|
-
let reported = false;
|
|
123
|
-
let warnedClone = false;
|
|
124
|
-
/**
|
|
125
|
-
* Reports each issue that was not present before. One that persists is
|
|
126
|
-
* reported once; one that goes away and comes back, again.
|
|
127
|
-
*/
|
|
128
|
-
const announce = () => {
|
|
129
|
-
const present = /* @__PURE__ */ new Set();
|
|
130
|
-
const fresh = [];
|
|
131
|
-
const note = (key, issue) => {
|
|
132
|
-
present.add(key);
|
|
133
|
-
if (!announced.has(key)) fresh.push(issue);
|
|
134
|
-
};
|
|
135
|
-
for (const error of status.errors) note(`error:${error.line ?? ""}:${error.message}`, {
|
|
136
|
-
kind: "error",
|
|
137
|
-
line: error.line,
|
|
138
|
-
message: error.message
|
|
139
|
-
});
|
|
140
|
-
for (const message of [...unregistered, ...status.issues]) note(`dropped:${message}`, {
|
|
141
|
-
kind: "dropped",
|
|
142
|
-
message
|
|
143
|
-
});
|
|
144
|
-
if (status.notice !== null) note(`notice:${status.notice}`, {
|
|
145
|
-
kind: "notice",
|
|
146
|
-
message: status.notice
|
|
147
|
-
});
|
|
148
|
-
announced = present;
|
|
149
|
-
const handler = issueHandler;
|
|
150
|
-
for (const issue of fresh) handler?.(issue);
|
|
151
|
-
};
|
|
152
|
-
const setStatus = (next) => {
|
|
153
|
-
status = next;
|
|
154
|
-
for (const listener of listeners) listener();
|
|
155
|
-
announce();
|
|
156
|
-
};
|
|
157
|
-
const drop = (issue) => {
|
|
158
|
-
if (dropped.length < MAX_ERRORS && !dropped.includes(issue)) dropped = [...dropped, issue];
|
|
159
|
-
};
|
|
160
|
-
const clearStartup = () => {
|
|
161
|
-
if (startup !== null) {
|
|
162
|
-
clearTimeout(startup);
|
|
163
|
-
startup = null;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
/** A frame that cannot run is a setup mistake: say so once, in the view and the console. */
|
|
167
|
-
const reportFrame = (problem) => {
|
|
168
|
-
clearStartup();
|
|
169
|
-
if (reported) return;
|
|
170
|
-
reported = true;
|
|
171
|
-
console.error(`Pickle: ${problem}`);
|
|
172
|
-
setStatus({
|
|
173
|
-
...status,
|
|
174
|
-
notice: problem
|
|
175
|
-
});
|
|
176
|
-
};
|
|
177
|
-
/** Fetches the frame again to tell a replaced policy from other reasons it stayed silent. */
|
|
178
|
-
const diagnoseFrame = () => {
|
|
179
|
-
startup = null;
|
|
180
|
-
const started = generation;
|
|
181
|
-
fetch(client.frame, { cache: "no-store" }).then((response) => frameProblem(client.frame, response, window.location.origin), () => frameProblem(client.frame, null, window.location.origin)).then((problem) => {
|
|
182
|
-
if (started === generation && !ready) reportFrame(problem);
|
|
183
|
-
});
|
|
184
|
-
};
|
|
185
|
-
/** `value` as the worker may receive it, or null when it cannot be copied (a function, say). */
|
|
186
|
-
const cloneable = (value) => {
|
|
187
|
-
try {
|
|
188
|
-
return structuredClone(value ?? null);
|
|
189
|
-
} catch {
|
|
190
|
-
if (!warnedClone) {
|
|
191
|
-
warnedClone = true;
|
|
192
|
-
console.warn("Pickle: an onAction handler returned a value structuredClone cannot copy (a function, a DOM node, a class with private fields), so the program's handle got null as its data.");
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
/** Gives a checked action to the page's handler and settles the handle with its outcome. */
|
|
198
|
-
const runAction = async (action) => {
|
|
199
|
-
const handler = actionHandler;
|
|
200
|
-
if (handler === void 0) return NO_ACTIONS;
|
|
201
|
-
try {
|
|
202
|
-
return {
|
|
203
|
-
data: cloneable(await handler(action)),
|
|
204
|
-
status: "success"
|
|
205
|
-
};
|
|
206
|
-
} catch (error) {
|
|
207
|
-
return {
|
|
208
|
-
error: parseErrorData(error) ?? {
|
|
209
|
-
code: "INTERNAL",
|
|
210
|
-
message: error instanceof Error ? error.message : String(error)
|
|
211
|
-
},
|
|
212
|
-
status: "error"
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
const post = (message) => {
|
|
217
|
-
if (ready && iframe?.contentWindow) iframe.contentWindow.postMessage(message, "*");
|
|
218
|
-
else queue.push(message);
|
|
219
|
-
};
|
|
220
|
-
const sendUpdate = (next, fresh) => {
|
|
221
|
-
seq++;
|
|
222
|
-
updates.set(seq, next);
|
|
223
|
-
post({
|
|
224
|
-
code: next.code,
|
|
225
|
-
fresh,
|
|
226
|
-
procedures: next.procedures,
|
|
227
|
-
prose: next.prose,
|
|
228
|
-
seq,
|
|
229
|
-
type: "update"
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
const sendQuery = (key, result) => {
|
|
233
|
-
const text = JSON.stringify(result);
|
|
234
|
-
if (sent.get(key) === text) return;
|
|
235
|
-
sent.set(key, text);
|
|
236
|
-
if (result.status === "pending") return;
|
|
237
|
-
seq++;
|
|
238
|
-
post({
|
|
239
|
-
key,
|
|
240
|
-
result,
|
|
241
|
-
seq,
|
|
242
|
-
type: "query"
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
const dropObservers = () => {
|
|
246
|
-
for (const unsubscribe of observers.values()) unsubscribe();
|
|
247
|
-
observers.clear();
|
|
248
|
-
sent.clear();
|
|
249
|
-
};
|
|
250
|
-
/** The running program's entry for `name`, if it is one of `kinds`. */
|
|
251
|
-
const procedure = (name, kinds) => {
|
|
252
|
-
if (running === null || !Object.hasOwn(running.procedures, name)) return;
|
|
253
|
-
const entry = running.procedures[name];
|
|
254
|
-
return entry !== void 0 && kinds.includes(entry.kind) ? entry : void 0;
|
|
255
|
-
};
|
|
256
|
-
/** The queries a result reads that the program declares, up to `MAX_QUERIES`. */
|
|
257
|
-
const checkedReads = (reads) => {
|
|
258
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
259
|
-
for (const read of reads) {
|
|
260
|
-
if (!isRecord(read) || typeof read.key !== "string" || typeof read.name !== "string" || procedure(read.name, ["query"]) === void 0) {
|
|
261
|
-
drop("The sandbox read a query this program does not declare");
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (!wanted.has(read.key) && wanted.size >= MAX_QUERIES) {
|
|
265
|
-
drop(`The sandbox read more than ${MAX_QUERIES} queries`);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
wanted.set(read.key, {
|
|
269
|
-
input: read.input,
|
|
270
|
-
key: read.key,
|
|
271
|
-
name: read.name
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return wanted;
|
|
275
|
-
};
|
|
276
|
-
const syncQueries = (wanted) => {
|
|
277
|
-
for (const [key, unsubscribe] of observers) if (!wanted.has(key)) {
|
|
278
|
-
unsubscribe();
|
|
279
|
-
observers.delete(key);
|
|
280
|
-
sent.delete(key);
|
|
281
|
-
}
|
|
282
|
-
for (const [key, read] of wanted) if (!observers.has(key)) observers.set(key, client.observe(read.name, read.input, (result) => {
|
|
283
|
-
sendQuery(key, result);
|
|
284
|
-
}));
|
|
285
|
-
};
|
|
286
|
-
/** Marks the call `answered` done; returns whether it was a handler call. */
|
|
287
|
-
const answer = (answered) => {
|
|
288
|
-
const node = calls.get(answered);
|
|
289
|
-
if (node === void 0) return false;
|
|
290
|
-
calls.delete(answered);
|
|
291
|
-
const left = (openCalls.get(node) ?? 1) - 1;
|
|
292
|
-
if (left > 0) openCalls.set(node, left);
|
|
293
|
-
else {
|
|
294
|
-
openCalls.delete(node);
|
|
295
|
-
settled.set(node, (settled.get(node) ?? 0) + 1);
|
|
296
|
-
}
|
|
297
|
-
return true;
|
|
298
|
-
};
|
|
299
|
-
const forgetCalls = () => {
|
|
300
|
-
calls.clear();
|
|
301
|
-
openCalls.clear();
|
|
302
|
-
settled.clear();
|
|
303
|
-
};
|
|
304
|
-
const runMutations = (mutations, fromHandler) => {
|
|
305
|
-
if (mutations.length > 0 && !fromHandler) {
|
|
306
|
-
drop("The sandbox asked for a mutation outside an event handler");
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (mutations.length > MAX_MUTATIONS) drop(`The sandbox asked for more than ${MAX_MUTATIONS} mutations at once`);
|
|
310
|
-
const started = generation;
|
|
311
|
-
for (const mutation of mutations.slice(0, MAX_MUTATIONS)) {
|
|
312
|
-
const call = isRecord(mutation) && typeof mutation.id === "number" && typeof mutation.name === "string" ? {
|
|
313
|
-
id: mutation.id,
|
|
314
|
-
input: mutation.input,
|
|
315
|
-
name: mutation.name
|
|
316
|
-
} : null;
|
|
317
|
-
const entry = call === null ? void 0 : procedure(call.name, ["action", "mutation"]);
|
|
318
|
-
if (call === null || entry === void 0) {
|
|
319
|
-
drop("The sandbox called a mutation this program does not declare");
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const kind = entry.kind === "action" ? "action" : "mutation";
|
|
323
|
-
client.mutate(call.name, call.input, {
|
|
324
|
-
confirm: confirmHandler,
|
|
325
|
-
invalidates: entry.invalidates,
|
|
326
|
-
kind,
|
|
327
|
-
signal: confirmationScope.signal
|
|
328
|
-
}).then((result) => kind === "action" && result.status === "success" && started === generation ? runAction({
|
|
329
|
-
input: result.data,
|
|
330
|
-
name: call.name
|
|
331
|
-
}) : result).then((result) => {
|
|
332
|
-
if (started === generation) {
|
|
333
|
-
seq++;
|
|
334
|
-
post({
|
|
335
|
-
id: call.id,
|
|
336
|
-
result,
|
|
337
|
-
seq,
|
|
338
|
-
type: "mutation"
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
const onMessage = (event) => {
|
|
345
|
-
if (iframe === null || event.source !== iframe.contentWindow) return;
|
|
346
|
-
const message = event.data;
|
|
347
|
-
if (!isFrameMessage(message)) return;
|
|
348
|
-
switch (message.type) {
|
|
349
|
-
case "unprotected":
|
|
350
|
-
reportFrame(unprotectedProblem(client.frame));
|
|
351
|
-
return;
|
|
352
|
-
case "ready": {
|
|
353
|
-
clearStartup();
|
|
354
|
-
ready = true;
|
|
355
|
-
const queued = queue;
|
|
356
|
-
queue = [];
|
|
357
|
-
for (const item of queued) post(item);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
case "result": {
|
|
361
|
-
const update = updates.get(message.seq);
|
|
362
|
-
updates.delete(message.seq);
|
|
363
|
-
if (update !== void 0) {
|
|
364
|
-
running = update;
|
|
365
|
-
lastGood = update;
|
|
366
|
-
dropped = [];
|
|
367
|
-
store.setSchemas(update);
|
|
368
|
-
}
|
|
369
|
-
store.apply(message.ops);
|
|
370
|
-
const fromHandler = answer(message.seq);
|
|
371
|
-
const wanted = checkedReads(message.queries);
|
|
372
|
-
runMutations(message.mutations, fromHandler);
|
|
373
|
-
const newest = update !== void 0 && update === latest && !hangs.has(update);
|
|
374
|
-
setStatus({
|
|
375
|
-
errors: parseErrors(message.errors),
|
|
376
|
-
issues: [...store.getIssues(), ...dropped],
|
|
377
|
-
notice: newest ? null : status.notice
|
|
378
|
-
});
|
|
379
|
-
syncQueries(wanted);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
case "failed": {
|
|
383
|
-
const failed = updates.get(message.seq);
|
|
384
|
-
updates.delete(message.seq);
|
|
385
|
-
answer(message.seq);
|
|
386
|
-
if (failed !== void 0 && failed === latest) latest = running;
|
|
387
|
-
setStatus({
|
|
388
|
-
...status,
|
|
389
|
-
notice: `Program failed (${message.message}); showing the last working version.`
|
|
390
|
-
});
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
case "quarantined": {
|
|
394
|
-
const failed = updates.get(message.seq);
|
|
395
|
-
const culprit = failed ?? running ?? latest;
|
|
396
|
-
updates.clear();
|
|
397
|
-
generation++;
|
|
398
|
-
withdrawConfirmations();
|
|
399
|
-
dropObservers();
|
|
400
|
-
forgetCalls();
|
|
401
|
-
store.reset();
|
|
402
|
-
running = null;
|
|
403
|
-
if (culprit !== null) {
|
|
404
|
-
const before = hangs.get(culprit) ?? 0;
|
|
405
|
-
hangs.set(culprit, failed === void 0 ? before + 1 : MAX_HANGS);
|
|
406
|
-
if (before === 0) console.warn(`Pickle: ${WATCHDOG_NOTICE}`);
|
|
407
|
-
}
|
|
408
|
-
const usable = (candidate) => candidate !== null && (hangs.get(candidate) ?? 0) < MAX_HANGS;
|
|
409
|
-
let recovery = null;
|
|
410
|
-
if (usable(latest)) recovery = latest;
|
|
411
|
-
else if (usable(lastGood)) recovery = lastGood;
|
|
412
|
-
setStatus({
|
|
413
|
-
errors: [],
|
|
414
|
-
issues: [],
|
|
415
|
-
notice: recovery === null ? GAVE_UP_NOTICE : WATCHDOG_NOTICE
|
|
416
|
-
});
|
|
417
|
-
if (recovery !== null) sendUpdate(recovery, false);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
return {
|
|
423
|
-
call: (node, fn, args) => {
|
|
424
|
-
seq++;
|
|
425
|
-
calls.set(seq, node);
|
|
426
|
-
openCalls.set(node, (openCalls.get(node) ?? 0) + 1);
|
|
427
|
-
post({
|
|
428
|
-
args,
|
|
429
|
-
fn,
|
|
430
|
-
seq,
|
|
431
|
-
type: "call"
|
|
432
|
-
});
|
|
433
|
-
},
|
|
434
|
-
childrenAllowed: (type) => running !== null && Object.hasOwn(running.components, type) && running.components[type]?.children !== "none",
|
|
435
|
-
getSettled: (node) => settled.get(node) ?? 0,
|
|
436
|
-
getStatus: () => status,
|
|
437
|
-
load: (next, document) => {
|
|
438
|
-
const fresh = document !== latestDocument;
|
|
439
|
-
const key = JSON.stringify(next);
|
|
440
|
-
if (key === latestKey && !fresh) return;
|
|
441
|
-
latestKey = key;
|
|
442
|
-
const checked = parseProgram(next);
|
|
443
|
-
if (checked === null) {
|
|
444
|
-
setStatus({
|
|
445
|
-
...status,
|
|
446
|
-
notice: FORMAT_NOTICE
|
|
447
|
-
});
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
latest = checked;
|
|
451
|
-
latestDocument = document;
|
|
452
|
-
if (fresh) withdrawConfirmations();
|
|
453
|
-
sendUpdate(checked, fresh);
|
|
454
|
-
},
|
|
455
|
-
propKind: (type, name) => {
|
|
456
|
-
const component = running !== null && Object.hasOwn(running.components, type) ? running.components[type] : void 0;
|
|
457
|
-
return component !== void 0 && Object.hasOwn(component.props, name) ? component.props[name]?.kind : void 0;
|
|
458
|
-
},
|
|
459
|
-
setActionHandler: (handler) => {
|
|
460
|
-
actionHandler = handler;
|
|
461
|
-
},
|
|
462
|
-
setConfirmHandler: (handler) => {
|
|
463
|
-
confirmHandler = handler;
|
|
464
|
-
},
|
|
465
|
-
setIssueHandler: (handler) => {
|
|
466
|
-
issueHandler = handler;
|
|
467
|
-
},
|
|
468
|
-
setUnregistered: (issues) => {
|
|
469
|
-
unregistered = issues;
|
|
470
|
-
announce();
|
|
471
|
-
},
|
|
472
|
-
start: () => {
|
|
473
|
-
if (iframe !== null) return;
|
|
474
|
-
const frame = document.createElement("iframe");
|
|
475
|
-
frame.setAttribute("sandbox", "allow-scripts");
|
|
476
|
-
frame.setAttribute("aria-hidden", "true");
|
|
477
|
-
frame.setAttribute("tabindex", "-1");
|
|
478
|
-
frame.style.display = "none";
|
|
479
|
-
frame.src = client.frame;
|
|
480
|
-
frame.addEventListener("load", () => {
|
|
481
|
-
if (iframe === frame && !ready && startup === null) startup = setTimeout(diagnoseFrame, STARTUP_MS);
|
|
482
|
-
});
|
|
483
|
-
iframe = frame;
|
|
484
|
-
window.addEventListener("message", onMessage);
|
|
485
|
-
document.body.append(frame);
|
|
486
|
-
},
|
|
487
|
-
stop: () => {
|
|
488
|
-
generation++;
|
|
489
|
-
window.removeEventListener("message", onMessage);
|
|
490
|
-
iframe?.remove();
|
|
491
|
-
iframe = null;
|
|
492
|
-
ready = false;
|
|
493
|
-
clearStartup();
|
|
494
|
-
reported = false;
|
|
495
|
-
queue = [];
|
|
496
|
-
updates.clear();
|
|
497
|
-
hangs = /* @__PURE__ */ new WeakMap();
|
|
498
|
-
forgetCalls();
|
|
499
|
-
dropped = [];
|
|
500
|
-
lastGood = null;
|
|
501
|
-
running = null;
|
|
502
|
-
latest = null;
|
|
503
|
-
latestKey = null;
|
|
504
|
-
latestDocument = void 0;
|
|
505
|
-
actionHandler = void 0;
|
|
506
|
-
confirmHandler = void 0;
|
|
507
|
-
withdrawConfirmations();
|
|
508
|
-
dropObservers();
|
|
509
|
-
store.reset();
|
|
510
|
-
status = idle;
|
|
511
|
-
},
|
|
512
|
-
store,
|
|
513
|
-
subscribe: (listener) => {
|
|
514
|
-
listeners.add(listener);
|
|
515
|
-
const unsubscribe = store.subscribe(listener);
|
|
516
|
-
return () => {
|
|
517
|
-
listeners.delete(listener);
|
|
518
|
-
unsubscribe();
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
//#endregion
|
|
524
|
-
export { createViewController };
|
package/dist/view/frame-check.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { frameSecurityPolicy } from "@jigs.design/runtime";
|
|
2
|
-
//#region src/view/frame-check.ts
|
|
3
|
-
const ADVICE = "Exclude that route from your page policy: `contentSecurityPolicy(host, { endpoint }).appliesTo(request.url)` is false for it.";
|
|
4
|
-
/**
|
|
5
|
-
* The issue for a frame that started and found its own policy missing: its
|
|
6
|
-
* scripts ran, so whatever replaced the policy does not block them, but the
|
|
7
|
-
* frame's network lock is gone and it refuses to run programs.
|
|
8
|
-
*/
|
|
9
|
-
function unprotectedProblem(frame) {
|
|
10
|
-
return `The sandbox frame's Content-Security-Policy is not in force on ${frame}, so the sandbox will not run. A page policy replaced it, or something stripped it. ${ADVICE}`;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* The issue for a frame that never said it was ready. `response` is the
|
|
14
|
-
* frame as the view fetched it again, or `null` when that fetch failed, as
|
|
15
|
-
* it does for an endpoint on another origin that does not list the page's
|
|
16
|
-
* origin, `pageOrigin`, in `allowedOrigins`. A response can carry several
|
|
17
|
-
* policies, which `Headers` joins with commas; the frame's own starts with
|
|
18
|
-
* `frameSecurityPolicy`.
|
|
19
|
-
*/
|
|
20
|
-
function frameProblem(frame, response, pageOrigin) {
|
|
21
|
-
if (response === null) {
|
|
22
|
-
if ((URL.canParse(frame, pageOrigin) ? new URL(frame, pageOrigin).origin : pageOrigin) !== pageOrigin) return `The sandbox frame at ${frame} did not start, and its response could not be read to say why. It is on another origin: check that the server's \`allowedOrigins\` lists ${pageOrigin}, which its frame-ancestors and CORS headers both need, and that nothing replaces the Content-Security-Policy it sends.`;
|
|
23
|
-
return `The sandbox frame at ${frame} did not start, and its response could not be read to say why. Check that nothing replaces the Content-Security-Policy it sends.`;
|
|
24
|
-
}
|
|
25
|
-
if (response.status !== 200) return `The sandbox frame at ${frame} answered ${response.status}, so the view cannot start. Check that the route hands GET requests to \`pickle.handler\`.`;
|
|
26
|
-
const policies = (response.headers.get("content-security-policy") ?? "").split(",").map((policy) => policy.trim()).filter((policy) => policy !== "");
|
|
27
|
-
if (!policies.some((policy) => policy.startsWith(frameSecurityPolicy))) return `The page's Content-Security-Policy replaced the sandbox frame's on ${frame}, so the sandbox will not run. ${ADVICE}`;
|
|
28
|
-
if (policies.length > 1) return `A second Content-Security-Policy is added to the sandbox frame's response on ${frame}, and it stops the frame from starting. ${ADVICE}`;
|
|
29
|
-
return `The sandbox frame at ${frame} did not start. Its policy is intact; check the browser console for the frame's own errors.`;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
export { frameProblem, unprotectedProblem };
|