@tsplc/debug 0.2.16 → 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/dist/format.js +36 -18
- package/dist/format.js.map +4 -4
- package/dist/index.js +768 -516
- package/dist/index.js.map +20 -17
- package/dist/manifest.js +6 -2
- package/dist/manifest.js.map +3 -3
- package/dist/paths.js +60 -28
- package/dist/paths.js.map +3 -3
- package/package.json +47 -46
- package/src/adapter.ts +43 -36
- package/src/api.ts +128 -126
- package/src/debugger.ts +584 -467
- package/src/errors.ts +3 -0
- package/src/hardware.ts +127 -96
- package/src/http.ts +147 -116
- package/src/index.ts +34 -4
- package/src/jobs.ts +102 -77
- package/src/login-value.ts +165 -0
- package/src/login.ts +233 -253
- package/src/manifest.ts +200 -194
- package/src/opcua.ts +55 -55
- package/src/paths.ts +230 -157
- package/src/requests.ts +86 -0
- package/src/telemetry.ts +104 -90
- package/src/timeline.ts +96 -61
- package/src/trace.ts +41 -31
- package/src/ui/bits.ts +45 -29
- package/src/ui/client.ts +33 -26
- package/src/ui/dom.ts +189 -108
- package/src/ui/format.ts +127 -72
- package/src/ui/hardware.ts +416 -343
- package/src/ui/header.ts +186 -164
- package/src/ui/index.ts +16 -9
- package/src/ui/jobs.ts +263 -214
- package/src/ui/main.ts +52 -48
- package/src/ui/overview.ts +152 -108
- package/src/ui/recent.ts +329 -276
- package/src/ui/requests.ts +69 -73
- package/src/ui/store.ts +236 -198
- package/src/ui/tree.ts +162 -130
- package/src/ui/variables.ts +557 -431
- package/src/watch.ts +22 -17
- package/src/ui/dev.ts +0 -579
- package/src/ui/fixture.ts +0 -548
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 = [
|
|
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) =>
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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. */
|