@tsplc/debug 0.0.0-stage → 0.2.16

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.
@@ -0,0 +1,78 @@
1
+ import type { AppRecord, ConnectionState, RequestTrace, TimelineEntry, TraceEvent } from "./api.ts";
2
+
3
+ export type Entry<Kind extends TimelineEntry["kind"]> = Extract<TimelineEntry, { kind: Kind }>;
4
+ type Details<Kind extends TimelineEntry["kind"]> = Omit<
5
+ Entry<Kind>,
6
+ "at" | "approximate" | "source" | "kind"
7
+ >;
8
+
9
+ /**
10
+ * Timeline entries by source. The PLC's records are placed on the app server's clock from their
11
+ * age when the server received them, and the server keeps its own clock, so those are
12
+ * approximate. The agent's entries reach a debugger only when the agent hosts it, on the same clock.
13
+ */
14
+ export const timelineEntries = {
15
+ app: (at: string, task: string, sequence: number, record: AppRecord): Entry<"app"> => ({
16
+ at,
17
+ approximate: true,
18
+ source: "plc",
19
+ kind: "app",
20
+ task,
21
+ sequence,
22
+ record,
23
+ }),
24
+ trace: (at: string, trace: TraceEvent): Entry<"trace"> => ({
25
+ at,
26
+ approximate: true,
27
+ source: "plc",
28
+ kind: "trace",
29
+ trace,
30
+ }),
31
+ request: (request: RequestTrace): Entry<"request"> => ({
32
+ at: request.startedAt,
33
+ approximate: true,
34
+ source: "server",
35
+ kind: "request",
36
+ request,
37
+ }),
38
+ deploy: (at: string, details: Details<"deploy">): Entry<"deploy"> => ({
39
+ at,
40
+ approximate: false,
41
+ source: "agent",
42
+ kind: "deploy",
43
+ ...details,
44
+ }),
45
+ audit: (at: string, details: Details<"audit">): Entry<"audit"> => ({
46
+ at,
47
+ approximate: false,
48
+ source: "agent",
49
+ kind: "audit",
50
+ ...details,
51
+ }),
52
+ connection: (at: string, state: ConnectionState, error?: string): Entry<"connection"> => ({
53
+ at,
54
+ approximate: false,
55
+ source: "debugger",
56
+ kind: "connection",
57
+ state,
58
+ ...(error && { error }),
59
+ }),
60
+ };
61
+
62
+ /** Entries from every source in time order, keeping the newest `limit`. */
63
+ export const createTimeline = (limit = 10_000) => {
64
+ const entries: TimelineEntry[] = [];
65
+ return {
66
+ add: (entry: TimelineEntry) => {
67
+ // Sources arrive late and out of order, so insert after the last entry not later than it.
68
+ let index = entries.length;
69
+ while (index > 0 && entry.at < (entries[index - 1]?.at ?? entry.at)) index--;
70
+ entries.splice(index, 0, entry);
71
+ if (entries.length > limit) entries.splice(0, entries.length - limit);
72
+ },
73
+ /** Entries after `at`, or all of them. */
74
+ since: (at?: string) => (at ? entries.filter((entry) => entry.at > at) : [...entries]),
75
+ };
76
+ };
77
+
78
+ export type Timeline = ReturnType<typeof createTimeline>;
package/src/trace.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { TelemetryRecord } from "@tsplc/server/telemetry";
2
+ import type { TraceEvent } from "./api.ts";
3
+ import type { DebugJob, DebugManifest } from "./manifest.ts";
4
+ import type { Paths } from "./paths.ts";
5
+
6
+ export type TraceRecord = Extract<TelemetryRecord, { kind: "trace" }>;
7
+
8
+ /**
9
+ * Decodes the opt-in trace through the manifest's event sites (docs/design/debugging.md). Only
10
+ * records of the manifest's own build may be decoded: another build's site ids mean other code.
11
+ */
12
+ export const createTraceDecoder = (manifest: DebugManifest, paths: Paths) => {
13
+ const sites = new Map(manifest.events.map((site) => [site.id, site]));
14
+ const jobs = new Map(manifest.jobs.map((job) => [job.id, job]));
15
+ const jobSites = new Map(
16
+ manifest.events.flatMap(({ id, job: jobId, phase }) => {
17
+ const job = jobId === undefined ? undefined : jobs.get(jobId);
18
+ return job && phase ? [[id, { job, started: phase === "started" }] as const] : [];
19
+ }),
20
+ );
21
+
22
+ return {
23
+ decode: (record: TraceRecord, task: string, sequence: number): TraceEvent | undefined => {
24
+ const site = sites.get(record.site);
25
+ if (!site) return undefined;
26
+ return {
27
+ sequence,
28
+ task,
29
+ scan: record.scan,
30
+ plcTime: record.time,
31
+ site: site.id,
32
+ value: paths.decode(site.type, record.value),
33
+ operands: site.operands.map(({ text, path, stable, type }, index) => ({
34
+ text,
35
+ ...(path && { path }),
36
+ stable,
37
+ value: paths.decode(type, record.operands[index]),
38
+ })),
39
+ };
40
+ },
41
+ /** The job a record starts or finishes, if its site is a job's. */
42
+ job: (record: TraceRecord): { job: DebugJob; started: boolean } | undefined =>
43
+ jobSites.get(record.site),
44
+ };
45
+ };
package/src/ui/bits.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { DebugChannel, DebugIo } from "../manifest.ts";
2
+
3
+ /** An unsigned little-endian field of an assembly, as EtherNet/IP lays them out; null past 32 bits or the end. */
4
+ export const readBits = (bytes: number[], offsetBits: number, bits: number): number | null => {
5
+ if (bits > 32 || offsetBits + bits > bytes.length * 8) return null;
6
+ let value = 0;
7
+ for (let bit = 0; bit < bits; bit++) {
8
+ const at = offsetBits + bit;
9
+ if ((bytes[at >> 3]! >> (at & 7)) & 1) value += 2 ** bit;
10
+ }
11
+ return value;
12
+ };
13
+
14
+ interface MappedBit {
15
+ channel: DebugChannel;
16
+ paths: string[];
17
+ }
18
+
19
+ /** What each bit of one assembly carries: the channel covering it and the TypeScript paths mapped to that channel. */
20
+ export const mapBits = (
21
+ device: string,
22
+ direction: "input" | "output",
23
+ bytes: number,
24
+ channels: DebugChannel[],
25
+ io: DebugIo[],
26
+ ): (MappedBit | undefined)[] => {
27
+ const bits: (MappedBit | undefined)[] = Array.from({ length: bytes * 8 }, () => undefined);
28
+ for (const channel of channels) {
29
+ if (channel.direction !== direction) continue;
30
+ const paths = io
31
+ .filter((entry) => entry.device === device && entry.channel === channel.id)
32
+ .map((entry) => entry.path);
33
+ for (
34
+ let bit = channel.offsetBits;
35
+ bit < channel.offsetBits + channel.bits && bit < bits.length;
36
+ bit++
37
+ )
38
+ bits[bit] = { channel, paths };
39
+ }
40
+ return bits;
41
+ };
@@ -0,0 +1,48 @@
1
+ import type { ApiResponses } from "../api.ts";
2
+
3
+ /**
4
+ * The debugger API, by relative URL so the UI works under the agent's `/debug` prefix. A `?token=` on the
5
+ * page is passed on to every call, since EventSource cannot send headers.
6
+ */
7
+ const token = new URLSearchParams(location.search).get("token");
8
+
9
+ type Method = "GET" | "PUT";
10
+ type RouteOf<M extends Method> = {
11
+ [K in keyof ApiResponses]: K extends `${M} /api/${infer Route}` ? Route : never;
12
+ }[keyof ApiResponses];
13
+ type ResponseOf<M extends Method, R extends string> = ApiResponses[Extract<
14
+ keyof ApiResponses,
15
+ `${M} /api/${R}`
16
+ >];
17
+
18
+ export const apiUrl = (route: string, params: Record<string, string> = {}) => {
19
+ const search = new URLSearchParams(params);
20
+ if (token) search.set("token", token);
21
+ const query = search.toString();
22
+ return `./api/${route}${query ? `?${query}` : ""}`;
23
+ };
24
+
25
+ const request = async <M extends Method, R extends RouteOf<M>>(
26
+ method: M,
27
+ route: R,
28
+ params?: Record<string, string>,
29
+ body?: unknown,
30
+ ): Promise<ResponseOf<M, R>> => {
31
+ const response = await fetch(apiUrl(route, params), {
32
+ method,
33
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
34
+ body: body === undefined ? undefined : JSON.stringify(body),
35
+ });
36
+ if (!response.ok) {
37
+ const detail = response.headers.get("Content-Type")?.includes("json")
38
+ ? (await response.json()).error
39
+ : await response.text();
40
+ throw new Error(`${method} ${route}: ${response.status} ${detail}`);
41
+ }
42
+ return response.json();
43
+ };
44
+
45
+ export const getJson = <R extends RouteOf<"GET">>(route: R, params?: Record<string, string>) =>
46
+ request("GET", route, params);
47
+ export const putJson = <R extends RouteOf<"PUT">>(route: R, body: unknown) =>
48
+ request("PUT", route, undefined, body);