@tsplc/debug 0.2.17 → 0.2.18

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/src/ui/header.ts CHANGED
@@ -2,178 +2,200 @@ import { h } from "./dom.ts";
2
2
  import { formatAge, formatClock, formatInterval } from "./format.ts";
3
3
  import type { Store, Topic } from "./store.ts";
4
4
 
5
- export const ROUTES = ["overview", "variables", "hardware", "jobs", "recent", "requests"] as const;
5
+ export const ROUTES = [
6
+ "overview",
7
+ "variables",
8
+ "hardware",
9
+ "jobs",
10
+ "recent",
11
+ "requests",
12
+ ] as const;
6
13
  export type Route = (typeof ROUTES)[number];
7
14
 
8
- const shortId = (id: string | null) => (id === null ? "unknown" : id.slice(0, 10));
15
+ const shortId = (id: string | null) =>
16
+ id === null ? "unknown" : id.slice(0, 10);
9
17
 
10
18
  const readout = (label: string, ...value: (Node | string)[]) =>
11
- h(
12
- "div",
13
- { class: "readout" },
14
- h("span", { class: "label" }, label),
15
- h("span", { class: "reading" }, ...value),
16
- );
19
+ h(
20
+ "div",
21
+ { class: "readout" },
22
+ h("span", { class: "label" }, label),
23
+ h("span", { class: "reading" }, ...value),
24
+ );
17
25
 
18
26
  /** Always visible: which build runs, how values are read and whether they can be, and how far to trust times. */
19
27
  export const header = (store: Store) => {
20
- const readouts = h("div", { class: "readouts" });
21
- const banners = h("div", { class: "banners" });
22
- const links = new Map(ROUTES.map((route) => [route, h("a", { href: `#/${route}` }, route)]));
23
- const element = h(
24
- "header",
25
- {},
26
- h(
27
- "div",
28
- { class: "bar" },
29
- h(
30
- "div",
31
- { class: "brand" },
32
- h("b", {}, "tsPLC"),
33
- h("span", {}, "debug"),
34
- h(
35
- "span",
36
- { class: "ro", title: "The debugger never writes or forces values" },
37
- "read-only",
38
- ),
39
- ),
40
- readouts,
41
- ),
42
- h("nav", {}, ...links.values()),
43
- banners,
44
- );
28
+ const readouts = h("div", { class: "readouts" });
29
+ const banners = h("div", { class: "banners" });
30
+ const links = new Map(
31
+ ROUTES.map((route) => [route, h("a", { href: `#/${route}` }, route)]),
32
+ );
33
+ const element = h(
34
+ "header",
35
+ {},
36
+ h(
37
+ "div",
38
+ { class: "bar" },
39
+ h(
40
+ "div",
41
+ { class: "brand" },
42
+ h("b", {}, "tsPLC"),
43
+ h("span", {}, "debug"),
44
+ h(
45
+ "span",
46
+ { class: "ro", title: "The debugger never writes or forces values" },
47
+ "read-only",
48
+ ),
49
+ ),
50
+ readouts,
51
+ ),
52
+ h("nav", {}, ...links.values()),
53
+ banners,
54
+ );
45
55
 
46
- const render = () => {
47
- const { session } = store;
48
- const { build, connection, reading } = session;
49
- const buildState = build.matches ? "match" : "mismatch";
50
- readouts.replaceChildren(
51
- readout(
52
- "build",
53
- h(
54
- "span",
55
- {
56
- class: `build ${buildState}`,
57
- title: `manifest ${build.manifest ?? "none"}\ncontroller ${build.controller ?? "unknown"}`,
58
- },
59
- `manifest ${shortId(build.manifest)}`,
60
- h("i", {}, build.matches ? " = " : " ≠ "),
61
- `controller ${shortId(build.controller)}`,
62
- ),
63
- ),
64
- readout(
65
- "values",
66
- h("span", { class: `dot c-${connection.state}`, title: connection.endpoint }),
67
- `${reading.method} · every ${formatInterval(reading.intervalMs)}`,
68
- h(
69
- "span",
70
- { class: "dim" },
71
- ` · ${connection.state} since ${formatClock(connection.since).slice(0, 8)}`,
72
- ` (${formatAge(store.ageOf(connection.since)).replace(" ago", "")})`,
73
- ),
74
- ),
75
- // The PLC clock counts from the runtime's start, so only the precision of the mapping means anything.
76
- readout(
77
- "plc clock",
78
- session.clock
79
- ? `±${session.clock.uncertaintyMs.toFixed(1)} ms against the app server's`
80
- : "not estimated",
81
- ),
82
- readout(
83
- "scans",
84
- ...session.tasks.map((task) => {
85
- const interval = store.manifest.tasks.find(
86
- (candidate) => candidate.name === task.name,
87
- )?.intervalMs;
88
- return h(
89
- "span",
90
- {
91
- class: "task",
92
- title:
93
- interval === undefined
94
- ? task.name
95
- : `${task.name}: configured every ${interval} ms`,
96
- },
97
- `${task.name} `,
98
- h("b", {}, task.scansPerSecond === null ? "—" : task.scansPerSecond.toFixed(1)),
99
- "/s",
100
- task.dropped > 0
101
- ? h(
102
- "span",
103
- {
104
- class: "err",
105
- title:
106
- "Telemetry records overwritten before the PLC sent them: Recent is missing these",
107
- },
108
- ` ${task.dropped} dropped`,
109
- )
110
- : "",
111
- );
112
- }),
113
- ),
114
- readout(
115
- "release",
116
- session.release
117
- ? h(
118
- "span",
119
- { title: `deployed ${session.release.deployedAt}` },
120
- `${session.release.id} · ${session.release.operation} · ${formatAge(store.ageOf(session.release.deployedAt))}`,
121
- )
122
- : "not reported",
123
- ),
124
- );
56
+ const render = () => {
57
+ const { session } = store;
58
+ const { build, connection, reading } = session;
59
+ const buildState = build.matches ? "match" : "mismatch";
60
+ readouts.replaceChildren(
61
+ readout(
62
+ "build",
63
+ h(
64
+ "span",
65
+ {
66
+ class: `build ${buildState}`,
67
+ title: `manifest ${build.manifest ?? "none"}\ncontroller ${build.controller ?? "unknown"}`,
68
+ },
69
+ `manifest ${shortId(build.manifest)}`,
70
+ h("i", {}, build.matches ? " = " : " ≠ "),
71
+ `controller ${shortId(build.controller)}`,
72
+ ),
73
+ ),
74
+ readout(
75
+ "values",
76
+ h("span", {
77
+ class: `dot c-${connection.state}`,
78
+ title: connection.endpoint,
79
+ }),
80
+ `${reading.method} · every ${formatInterval(reading.intervalMs)}`,
81
+ h(
82
+ "span",
83
+ { class: "dim" },
84
+ ` · ${connection.state} since ${formatClock(connection.since).slice(0, 8)}`,
85
+ ` (${formatAge(store.ageOf(connection.since)).replace(" ago", "")})`,
86
+ ),
87
+ ),
88
+ // The PLC clock counts from the runtime's start, so only the precision of the mapping means anything.
89
+ readout(
90
+ "plc clock",
91
+ session.clock
92
+ ? `±${session.clock.uncertaintyMs.toFixed(1)} ms against the app server's`
93
+ : "not estimated",
94
+ ),
95
+ readout(
96
+ "scans",
97
+ ...session.tasks.map((task) => {
98
+ const interval = store.manifest.tasks.find(
99
+ (candidate) => candidate.name === task.name,
100
+ )?.intervalMs;
101
+ return h(
102
+ "span",
103
+ {
104
+ class: "task",
105
+ title:
106
+ interval === undefined
107
+ ? task.name
108
+ : `${task.name}: configured every ${interval} ms`,
109
+ },
110
+ `${task.name} `,
111
+ h(
112
+ "b",
113
+ {},
114
+ task.scansPerSecond === null
115
+ ? "—"
116
+ : task.scansPerSecond.toFixed(1),
117
+ ),
118
+ "/s",
119
+ task.dropped > 0
120
+ ? h(
121
+ "span",
122
+ {
123
+ class: "err",
124
+ title:
125
+ "Telemetry records overwritten before the PLC sent them: Recent is missing these",
126
+ },
127
+ ` ${task.dropped} dropped`,
128
+ )
129
+ : "",
130
+ );
131
+ }),
132
+ ),
133
+ readout(
134
+ "release",
135
+ session.release
136
+ ? h(
137
+ "span",
138
+ { title: `deployed ${session.release.deployedAt}` },
139
+ `${session.release.id} · ${session.release.operation} · ${formatAge(store.ageOf(session.release.deployedAt))}`,
140
+ )
141
+ : "not reported",
142
+ ),
143
+ );
125
144
 
126
- const notices: [string, string][] = [];
127
- if (!build.matches)
128
- notices.push([
129
- "danger",
130
- build.controller === null
131
- ? "The controller's build id could not be read, so this manifest may not describe what is running."
132
- : `Build mismatch: the controller runs ${build.controller}, but this manifest is ${build.manifest ?? "missing"}. Paths, values and source links may be wrong.`,
133
- ]);
134
- if (store.stream === "reconnecting")
135
- notices.push([
136
- "warn",
137
- "Lost the stream from the debugger host; reconnecting. Every value is stale until it resumes.",
138
- ]);
139
- else if (connection.state !== "connected")
140
- notices.push([
141
- "warn",
142
- `${connection.error ?? `Reading values: ${connection.state} since ${formatClock(connection.since)}.`} Values shown are the last read and marked stale.`,
143
- ]);
144
- if (store.error) notices.push(["error", store.error]);
145
- banners.replaceChildren(
146
- ...notices.map(([tone, text]) =>
147
- h(
148
- "div",
149
- { class: `banner ${tone}` },
150
- text,
151
- tone === "error"
152
- ? h(
153
- "button",
154
- { onclick: () => ((store.error = undefined), store.notify("error")) },
155
- "dismiss",
156
- )
157
- : "",
158
- ),
159
- ),
160
- );
161
- };
145
+ const notices: [string, string][] = [];
146
+ if (!build.matches) {
147
+ notices.push([
148
+ "danger",
149
+ build.controller === null
150
+ ? "The controller's build id could not be read, so this manifest may not describe what is running."
151
+ : `Build mismatch: the controller runs ${build.controller}, but this manifest is ${build.manifest ?? "missing"}. Paths, values and source links may be wrong.`,
152
+ ]);
153
+ }
154
+ if (store.stream === "reconnecting") {
155
+ notices.push([
156
+ "warn",
157
+ "Lost the stream from the debugger host; reconnecting. Every value is stale until it resumes.",
158
+ ]);
159
+ } else if (connection.state !== "connected") {
160
+ notices.push([
161
+ "warn",
162
+ `${connection.error ?? `Reading values: ${connection.state} since ${formatClock(connection.since)}.`} Values shown are the last read and marked stale.`,
163
+ ]);
164
+ }
165
+ if (store.error) {
166
+ notices.push(["error", store.error]);
167
+ }
168
+ banners.replaceChildren(
169
+ ...notices.map(([tone, text]) =>
170
+ h(
171
+ "div",
172
+ { class: `banner ${tone}` },
173
+ text,
174
+ tone === "error"
175
+ ? h("button", { onclick: () => store.clearError() }, "dismiss")
176
+ : "",
177
+ ),
178
+ ),
179
+ );
180
+ };
162
181
 
163
- render();
164
- return {
165
- element,
166
- update(topics: Set<Topic>) {
167
- if (
168
- topics.has("session") ||
169
- topics.has("stream") ||
170
- topics.has("error") ||
171
- topics.has("clock")
172
- )
173
- render();
174
- },
175
- setRoute(route: Route) {
176
- for (const [name, link] of links) link.classList.toggle("on", name === route);
177
- },
178
- };
182
+ render();
183
+ return {
184
+ element,
185
+ update(topics: Set<Topic>) {
186
+ if (
187
+ topics.has("session") ||
188
+ topics.has("stream") ||
189
+ topics.has("error") ||
190
+ topics.has("clock")
191
+ ) {
192
+ render();
193
+ }
194
+ },
195
+ setRoute(route: Route) {
196
+ for (const [name, link] of links) {
197
+ link.classList.toggle("on", name === route);
198
+ }
199
+ },
200
+ };
179
201
  };
package/src/ui/index.ts CHANGED
@@ -5,15 +5,22 @@ import { html } from "./page.ts";
5
5
  let ui: Promise<{ html: string; script: string }> | undefined;
6
6
 
7
7
  const build = async () => {
8
- const result = await Bun.build({
9
- entrypoints: [
10
- resolve(dirname(createRequire(import.meta.url).resolve("@tsplc/debug")), "../src/ui/main.ts"),
11
- ],
12
- target: "browser",
13
- minify: true,
14
- });
15
- if (!result.success) throw new AggregateError(result.logs, "Bundling the debugger UI failed");
16
- return { html, script: await result.outputs[0]!.text() };
8
+ const result = await Bun.build({
9
+ entrypoints: [
10
+ resolve(
11
+ dirname(createRequire(import.meta.url).resolve("@tsplc/debug")),
12
+ "../src/ui/main.ts",
13
+ ),
14
+ ],
15
+ target: "browser",
16
+ // Workspace packages resolve to their sources, as they do for the host, so no build is needed first.
17
+ conditions: ["bun"],
18
+ minify: true,
19
+ });
20
+ if (!result.success) {
21
+ throw new AggregateError(result.logs, "Bundling the debugger UI failed");
22
+ }
23
+ return { html, script: await result.outputs[0]!.text() };
17
24
  };
18
25
 
19
26
  /** The browser UI: serve `html` at `GET /` and `script` at `GET /ui.js`. Bundled on first call, then cached. */