@tsplc/debug 0.0.0-stage → 0.2.17

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/index.js ADDED
@@ -0,0 +1,1681 @@
1
+ // packages/debug/src/debugger.ts
2
+ import { join } from "node:path";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { telemetryBatch } from "@tsplc/server/telemetry";
5
+ import { z as z2 } from "zod";
6
+
7
+ // packages/debug/src/adapter.ts
8
+ var qualityRank = { good: 0, uncertain: 1, bad: 2, unavailable: 3 };
9
+ var worstQuality = (samples) => samples.reduce((worst, sample) => {
10
+ const quality = sample?.quality ?? "unavailable";
11
+ return qualityRank[quality] > qualityRank[worst] ? quality : worst;
12
+ }, "good");
13
+
14
+ // packages/debug/src/hardware.ts
15
+ var faultyState = /error|fault|fail|timeout|lost|closed/i;
16
+ var untyped = { kind: "scalar", iec: "ANY" };
17
+ var healthReads = (device) => {
18
+ const { state, states, diagnostic, message } = device.health ?? {};
19
+ return [
20
+ ...state ? [
21
+ {
22
+ target: state,
23
+ type: states ? { kind: "enum", name: "state", members: states } : { kind: "scalar", iec: "DINT" }
24
+ }
25
+ ] : [],
26
+ ...diagnostic ? [{ target: diagnostic, type: { kind: "scalar", iec: "BOOL" } }] : [],
27
+ ...message ? [{ target: message, type: { kind: "scalar", iec: "STRING" } }] : []
28
+ ];
29
+ };
30
+ var deviceReads = (device, io, typeOf) => [
31
+ ...healthReads(device),
32
+ ...device.assemblies.flatMap(({ raw, bytes }) => raw ? [
33
+ {
34
+ target: raw,
35
+ type: { kind: "array", element: "BYTE", length: bytes, lower: 0 }
36
+ }
37
+ ] : []),
38
+ ...io.filter((binding) => binding.device === device.id).map(({ target, type }) => ({ target, type: typeOf(type) })),
39
+ ...Object.values(device.status ?? {}).map((target) => ({ target, type: untyped }))
40
+ ];
41
+ var health = (device, value) => {
42
+ const { state, states, diagnostic, message } = device.health ?? {};
43
+ if (!state && !diagnostic)
44
+ return { state: "unknown", quality: "unavailable" };
45
+ const stateSample = state ? value(state) : undefined;
46
+ const diagnosticSample = diagnostic ? value(diagnostic) : undefined;
47
+ const messageSample = message ? value(message) : undefined;
48
+ const quality = worstQuality([
49
+ ...state ? [stateSample] : [],
50
+ ...diagnostic ? [diagnosticSample] : []
51
+ ]);
52
+ const number = typeof stateSample?.value === "number" ? stateSample.value : undefined;
53
+ const name = Object.entries(states ?? {}).find(([, member]) => member === number)?.[0] ?? (typeof stateSample?.value === "string" ? stateSample.value : undefined);
54
+ const faulted = diagnosticSample?.value === true || name !== undefined && faultyState.test(name);
55
+ return {
56
+ state: quality !== "good" ? "stale" : faulted ? "fault" : "ok",
57
+ ...number !== undefined && { value: number },
58
+ ...name && { name },
59
+ ...typeof diagnosticSample?.value === "boolean" && { diagnostic: diagnosticSample.value },
60
+ ...typeof messageSample?.value === "string" && { message: messageSample.value },
61
+ quality
62
+ };
63
+ };
64
+ var deviceState = (device, io, value) => ({
65
+ id: device.id,
66
+ health: health(device, value),
67
+ assemblies: device.assemblies.map(({ direction, address, raw }) => {
68
+ const sample = raw ? value(raw) : undefined;
69
+ return {
70
+ direction,
71
+ ...address && { address },
72
+ bytes: Array.isArray(sample?.value) && sample.value.every((byte) => typeof byte === "number") ? sample.value : null,
73
+ quality: sample?.quality ?? "unavailable"
74
+ };
75
+ }),
76
+ channels: io.flatMap((binding) => binding.device === device.id && binding.channel ? [
77
+ {
78
+ id: binding.channel,
79
+ path: binding.path,
80
+ value: value(binding.target)?.value ?? null,
81
+ quality: value(binding.target)?.quality ?? "unavailable"
82
+ }
83
+ ] : []),
84
+ status: Object.fromEntries(Object.entries(device.status ?? {}).map(([label, target]) => [
85
+ label,
86
+ { value: value(target)?.value ?? null, quality: value(target)?.quality ?? "unavailable" }
87
+ ]))
88
+ });
89
+
90
+ // packages/debug/src/jobs.ts
91
+ var scalar = (iec) => ({ kind: "scalar", iec });
92
+ var busyRead = (job) => ({
93
+ target: `${job.target}.busy`,
94
+ type: scalar("BOOL")
95
+ });
96
+ var detailReads = (job) => [
97
+ { target: `${job.target}.step`, type: scalar("UINT") },
98
+ { target: `${job.target}.cancelling`, type: scalar("BOOL") },
99
+ ...job.startedAt ? [{ target: job.startedAt, type: scalar("UDINT") }] : [],
100
+ ...job.requests.map(({ sequence }) => ({ target: sequence, type: scalar("UDINT") }))
101
+ ];
102
+ var busyOf = (job, value, traced) => {
103
+ const read = value(busyRead(job).target);
104
+ return read?.quality === "good" ? read.value === true : traced?.busy ?? null;
105
+ };
106
+ var numberOf = (sample) => sample?.quality === "good" && typeof sample.value === "number" ? sample.value : undefined;
107
+ var jobState = (job, inputs) => {
108
+ const { value, traced, clock, now } = inputs;
109
+ const busy = busyOf(job, value, traced);
110
+ const startedPlc = job.startedAt === undefined ? undefined : numberOf(value(job.startedAt));
111
+ const startedAt = traced?.startedAt ?? (clock && startedPlc !== undefined ? new Date(startedPlc - clock.offsetMs).toISOString() : undefined);
112
+ const base = {
113
+ id: job.id,
114
+ quality: value(busyRead(job).target)?.quality ?? (traced ? "good" : "unavailable"),
115
+ busy,
116
+ ...startedAt && { startedAt },
117
+ ...inputs.failure && { failure: inputs.failure }
118
+ };
119
+ if (!busy)
120
+ return base;
121
+ if (inputs.unread)
122
+ return { ...base, unread: true };
123
+ const step = numberOf(value(`${job.target}.step`));
124
+ const current = job.steps.find((candidate) => candidate.step === step);
125
+ const calls = job.requests.map((call) => {
126
+ const sequence = numberOf(value(call.sequence)) ?? 0;
127
+ const trace = inputs.request({ job: call.job ?? job.id, service: call.service, sequence });
128
+ return { service: call.service, sequence, trace };
129
+ }).filter(({ sequence }) => sequence > 0);
130
+ const later = (call, than) => call.trace && than.trace ? call.trace.startedAt > than.trace.startedAt : call.sequence > than.sequence;
131
+ const latest = calls.reduce((latest, call) => latest && !later(call, latest) ? latest : call, undefined);
132
+ const cancelling = value(`${job.target}.cancelling`);
133
+ return {
134
+ ...base,
135
+ ...cancelling?.quality === "good" && { cancelling: cancelling.value === true },
136
+ ...step !== undefined && { step },
137
+ ...current && { current: { kind: current.kind, range: current.range, text: current.text } },
138
+ ...startedAt && { elapsedMs: Math.max(0, now - Date.parse(startedAt)) },
139
+ ...latest && {
140
+ request: {
141
+ service: latest.service,
142
+ sequence: latest.sequence,
143
+ ...latest.trace && { trace: latest.trace }
144
+ }
145
+ }
146
+ };
147
+ };
148
+
149
+ // packages/debug/src/paths.ts
150
+ var segmentPattern = /[^.[\]]+|\[(?:-?\d+|\*)\]/g;
151
+ var pathSegments = (path) => path.match(segmentPattern) ?? [];
152
+ var joinPath = (prefix, segment) => segment.startsWith("[") || !prefix ? `${prefix}${segment}` : `${prefix}.${segment}`;
153
+ var typeOf = (manifest, id) => manifest.types[id] ?? { kind: "scalar", iec: id };
154
+ var createPaths = (manifest) => {
155
+ const typeOfId = (id) => typeOf(manifest, id);
156
+ const bases = new Map([
157
+ ...manifest.roots.map(({ path, ...root }) => [path, { kind: "target", path, ...root }]),
158
+ ...manifest.io.map(({ path, target, type }) => [path, { kind: "target", path, target, type, readable: true }])
159
+ ]);
160
+ const constants = new Map(manifest.constants.map(({ path, ...constant }) => [
161
+ path,
162
+ { kind: "constant", path, ...constant }
163
+ ]));
164
+ const groups = new Map;
165
+ for (const path of [...bases.keys(), ...constants.keys()]) {
166
+ let prefix = "";
167
+ for (const segment of pathSegments(path)) {
168
+ let children = groups.get(prefix);
169
+ if (!children)
170
+ groups.set(prefix, children = new Set);
171
+ children.add(segment);
172
+ prefix = joinPath(prefix, segment);
173
+ }
174
+ }
175
+ const resolve = (path) => {
176
+ const constant = constants.get(path);
177
+ if (constant)
178
+ return constant;
179
+ const segments = pathSegments(path);
180
+ for (let length = segments.length;length > 0; length--) {
181
+ const base = bases.get(segments.slice(0, length).reduce(joinPath, ""));
182
+ if (!base)
183
+ continue;
184
+ let resolved = base;
185
+ for (const segment of segments.slice(length)) {
186
+ const type = typeOfId(resolved.type);
187
+ if (type.kind === "record") {
188
+ const field = type.fields.find(({ name }) => name === segment);
189
+ if (!field)
190
+ return;
191
+ resolved = {
192
+ kind: "target",
193
+ path: joinPath(resolved.path, segment),
194
+ target: `${resolved.target}.${field.target}`,
195
+ type: field.type,
196
+ readable: resolved.readable,
197
+ ...field.generated && { generated: true },
198
+ ...field.declaration && { declaration: field.declaration }
199
+ };
200
+ } else if (type.kind === "array") {
201
+ const index = /^\[(-?\d+)\]$/.exec(segment)?.[1];
202
+ if (index === undefined || Number(index) < type.lower || Number(index) >= type.lower + type.length)
203
+ return;
204
+ resolved = {
205
+ kind: "target",
206
+ path: joinPath(resolved.path, segment),
207
+ target: `${resolved.target}[${index}]`,
208
+ type: type.element,
209
+ readable: resolved.readable
210
+ };
211
+ } else
212
+ return;
213
+ }
214
+ return resolved;
215
+ }
216
+ };
217
+ const typedChildren = (resolved) => {
218
+ const type = resolved?.kind === "target" ? typeOfId(resolved.type) : undefined;
219
+ if (type?.kind === "record")
220
+ return type.fields.map(({ name }) => name);
221
+ if (type?.kind === "array")
222
+ return Array.from({ length: type.length }, (_, index) => `[${type.lower + index}]`);
223
+ return [];
224
+ };
225
+ const children = (path) => {
226
+ const names = [...typedChildren(resolve(path)), ...groups.get(path) ?? []];
227
+ return [...new Set(names)].map((name) => {
228
+ const child = joinPath(path, name);
229
+ return { name, ...resolve(child) ?? { kind: "group", path: child } };
230
+ });
231
+ };
232
+ const isLeaf = (node) => node.kind === "constant" || node.kind === "target" && ["scalar", "enum"].includes(typeOfId(node.type).kind);
233
+ const expand = (patterns, limit = 2000) => {
234
+ const leaves = new Set;
235
+ const add = (path) => {
236
+ leaves.add(path);
237
+ if (leaves.size > limit)
238
+ throw new RangeError(`More than ${limit} values match`);
239
+ };
240
+ const below = (node) => {
241
+ if (isLeaf(node))
242
+ return add(node.path);
243
+ for (const child of children(node.path))
244
+ if (!(child.kind === "target" && child.generated))
245
+ below(child);
246
+ };
247
+ const match = (prefix, rest, wildcard) => {
248
+ const [head, ...tail] = rest;
249
+ if (head === undefined) {
250
+ const node = resolve(prefix) ?? (groups.has(prefix) ? { kind: "group", path: prefix } : undefined);
251
+ if (node)
252
+ below(node);
253
+ else if (!wildcard)
254
+ add(prefix);
255
+ return;
256
+ }
257
+ if (head === "*" || head === "[*]") {
258
+ for (const child of children(prefix))
259
+ if (!(child.kind === "target" && child.generated) && (head === "*" || child.name.startsWith("[")))
260
+ match(child.path, tail, true);
261
+ } else
262
+ match(joinPath(prefix, head), tail, wildcard);
263
+ };
264
+ for (const pattern of patterns)
265
+ match("", pathSegments(pattern), false);
266
+ return [...leaves];
267
+ };
268
+ const decode = (typeId, raw) => {
269
+ const type = typeOfId(typeId);
270
+ if (type.kind === "enum")
271
+ return Object.entries(type.members).find(([, value]) => value === raw)?.[0] ?? raw;
272
+ if (type.kind === "scalar" && type.iec === "BOOL" && typeof raw === "number")
273
+ return raw !== 0;
274
+ return raw;
275
+ };
276
+ return { typeOf: typeOfId, resolve, children, isLeaf, expand, decode };
277
+ };
278
+
279
+ // packages/debug/src/telemetry.ts
280
+ import { recordedAt } from "@tsplc/server/telemetry-log";
281
+ var clockWindowMs = 60000;
282
+ var createTelemetryFollower = (manifest) => {
283
+ const intervals = new Map(manifest.tasks.map(({ name, intervalMs }) => [name, intervalMs]));
284
+ const tasks = new Map(manifest.tasks.map(({ name }) => [
285
+ name,
286
+ { name, scan: null, scansPerSecond: null, dropped: 0 }
287
+ ]));
288
+ const rates = new Map;
289
+ let estimates = [];
290
+ let newest = 0;
291
+ let build = null;
292
+ const clock = () => {
293
+ if (!estimates.length)
294
+ return null;
295
+ const offsets = estimates.map(({ offsetMs }) => offsetMs);
296
+ const best = estimates.reduce((best, estimate) => estimate.offsetMs > best.offsetMs ? estimate : best);
297
+ return {
298
+ offsetMs: best.offsetMs,
299
+ uncertaintyMs: best.offsetMs - Math.min(...offsets) + best.intervalMs
300
+ };
301
+ };
302
+ const observe = (batch, receivedAt) => {
303
+ const rate = rates.get(batch.task);
304
+ const restarted = rate !== undefined && batch.now < rate.now;
305
+ if (restarted)
306
+ estimates = [];
307
+ estimates = estimates.filter(({ at }) => receivedAt - at < clockWindowMs);
308
+ estimates.push({
309
+ offsetMs: batch.now - receivedAt,
310
+ intervalMs: intervals.get(batch.task) ?? 0,
311
+ at: receivedAt
312
+ });
313
+ const stats = {
314
+ name: batch.task,
315
+ scansPerSecond: tasks.get(batch.task)?.scansPerSecond ?? null,
316
+ scan: batch.scan,
317
+ dropped: batch.dropped
318
+ };
319
+ if (!rate || restarted)
320
+ rates.set(batch.task, { scan: batch.scan, now: batch.now });
321
+ else if (batch.now - rate.now >= 1000) {
322
+ stats.scansPerSecond = (batch.scan - rate.scan) * 1000 / (batch.now - rate.now);
323
+ rates.set(batch.task, { scan: batch.scan, now: batch.now });
324
+ }
325
+ tasks.set(batch.task, stats);
326
+ };
327
+ return {
328
+ since: () => newest,
329
+ accept: (reply) => {
330
+ if (reply.latest < newest) {
331
+ newest = 0;
332
+ return [];
333
+ }
334
+ const received = [];
335
+ for (const batch of reply.batches) {
336
+ if (batch.seq <= newest)
337
+ continue;
338
+ newest = batch.seq;
339
+ build = batch.build;
340
+ const receivedAt = new Date(batch.receivedAt);
341
+ observe(batch, receivedAt.getTime());
342
+ batch.records.forEach((record, index) => received.push({
343
+ build: batch.build,
344
+ task: batch.task,
345
+ sequence: batch.first + index,
346
+ at: recordedAt(batch, record, receivedAt).toISOString(),
347
+ record
348
+ }));
349
+ }
350
+ return received;
351
+ },
352
+ build: () => build,
353
+ clock,
354
+ tasks: () => [...tasks.values()].map((task) => ({ ...task }))
355
+ };
356
+ };
357
+
358
+ // packages/debug/src/timeline.ts
359
+ var timelineEntries = {
360
+ app: (at, task, sequence, record) => ({
361
+ at,
362
+ approximate: true,
363
+ source: "plc",
364
+ kind: "app",
365
+ task,
366
+ sequence,
367
+ record
368
+ }),
369
+ trace: (at, trace) => ({
370
+ at,
371
+ approximate: true,
372
+ source: "plc",
373
+ kind: "trace",
374
+ trace
375
+ }),
376
+ request: (request) => ({
377
+ at: request.startedAt,
378
+ approximate: true,
379
+ source: "server",
380
+ kind: "request",
381
+ request
382
+ }),
383
+ deploy: (at, details) => ({
384
+ at,
385
+ approximate: false,
386
+ source: "agent",
387
+ kind: "deploy",
388
+ ...details
389
+ }),
390
+ audit: (at, details) => ({
391
+ at,
392
+ approximate: false,
393
+ source: "agent",
394
+ kind: "audit",
395
+ ...details
396
+ }),
397
+ connection: (at, state, error) => ({
398
+ at,
399
+ approximate: false,
400
+ source: "debugger",
401
+ kind: "connection",
402
+ state,
403
+ ...error && { error }
404
+ })
405
+ };
406
+ var createTimeline = (limit = 1e4) => {
407
+ const entries = [];
408
+ return {
409
+ add: (entry) => {
410
+ let index = entries.length;
411
+ while (index > 0 && entry.at < (entries[index - 1]?.at ?? entry.at))
412
+ index--;
413
+ entries.splice(index, 0, entry);
414
+ if (entries.length > limit)
415
+ entries.splice(0, entries.length - limit);
416
+ },
417
+ since: (at) => at ? entries.filter((entry) => entry.at > at) : [...entries]
418
+ };
419
+ };
420
+
421
+ // packages/debug/src/trace.ts
422
+ var createTraceDecoder = (manifest, paths) => {
423
+ const sites = new Map(manifest.events.map((site) => [site.id, site]));
424
+ const jobs = new Map(manifest.jobs.map((job) => [job.id, job]));
425
+ const jobSites = new Map(manifest.events.flatMap(({ id, job: jobId, phase }) => {
426
+ const job = jobId === undefined ? undefined : jobs.get(jobId);
427
+ return job && phase ? [[id, { job, started: phase === "started" }]] : [];
428
+ }));
429
+ return {
430
+ decode: (record, task, sequence) => {
431
+ const site = sites.get(record.site);
432
+ if (!site)
433
+ return;
434
+ return {
435
+ sequence,
436
+ task,
437
+ scan: record.scan,
438
+ plcTime: record.time,
439
+ site: site.id,
440
+ value: paths.decode(site.type, record.value),
441
+ operands: site.operands.map(({ text, path, stable, type }, index) => ({
442
+ text,
443
+ ...path && { path },
444
+ stable,
445
+ value: paths.decode(type, record.operands[index])
446
+ }))
447
+ };
448
+ },
449
+ job: (record) => jobSites.get(record.site)
450
+ };
451
+ };
452
+
453
+ // packages/debug/src/watch.ts
454
+ import { existsSync } from "node:fs";
455
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
456
+ import { dirname } from "node:path";
457
+ import { isDeepStrictEqual } from "node:util";
458
+ import { z } from "zod";
459
+ var watchGroupsSchema = z.strictObject({
460
+ groups: z.record(z.string().min(1), z.array(z.string().min(1)))
461
+ });
462
+ var loadWatchGroups = async (file, configured) => {
463
+ let saved = existsSync(file) ? watchGroupsSchema.parse(JSON.parse(await readFile(file, "utf8"))).groups : {};
464
+ const groups = () => ({ groups: { ...configured, ...saved } });
465
+ return {
466
+ groups,
467
+ save: async ({ groups: next }) => {
468
+ saved = Object.fromEntries(Object.entries(next).filter(([name, paths]) => !isDeepStrictEqual(configured[name], paths)));
469
+ await mkdir(dirname(file), { recursive: true });
470
+ await writeFile(file, JSON.stringify({ groups: saved }, null, 2));
471
+ return groups();
472
+ }
473
+ };
474
+ };
475
+
476
+ // packages/debug/src/debugger.ts
477
+ class DebugError extends Error {
478
+ status;
479
+ constructor(message, status) {
480
+ super(message);
481
+ this.status = status;
482
+ }
483
+ }
484
+ var pullMs = 1000;
485
+ var buildCheckMs = 1e4;
486
+ var hardwareMs = 5000;
487
+ var detailedJobs = 16;
488
+ var requestsRemembered = 1000;
489
+ var requestTrace = z2.object({
490
+ seq: z2.number().int(),
491
+ service: z2.string(),
492
+ job: z2.number().int().optional(),
493
+ sequence: z2.number().int().optional(),
494
+ startedAt: z2.string(),
495
+ durationMs: z2.number().optional(),
496
+ status: z2.enum(["pending", "ok", "error", "timeout"]),
497
+ httpStatus: z2.number().int().optional(),
498
+ error: z2.string().optional(),
499
+ input: z2.unknown().optional(),
500
+ output: z2.unknown().optional()
501
+ });
502
+ var requestsReply = z2.object({ requests: z2.array(requestTrace), latest: z2.number().int() });
503
+ var telemetryReply = z2.object({
504
+ batches: z2.array(telemetryBatch.extend({ seq: z2.number().int(), receivedAt: z2.string() })),
505
+ latest: z2.number().int()
506
+ });
507
+ var message = (error) => error instanceof Error ? error.message : String(error);
508
+ var createShown = () => {
509
+ const counts = new Map;
510
+ return {
511
+ has: (key) => counts.has(key),
512
+ add: (keys) => {
513
+ for (const key of keys)
514
+ counts.set(key, (counts.get(key) ?? 0) + 1);
515
+ return () => {
516
+ for (const key of keys) {
517
+ const count = counts.get(key) - 1;
518
+ if (count)
519
+ counts.set(key, count);
520
+ else
521
+ counts.delete(key);
522
+ }
523
+ };
524
+ }
525
+ };
526
+ };
527
+ var createDebugger = async (options) => {
528
+ const { manifest, adapter } = options;
529
+ const paths = createPaths(manifest);
530
+ const trace = createTraceDecoder(manifest, paths);
531
+ const telemetry = createTelemetryFollower(manifest);
532
+ const timeline = createTimeline();
533
+ const watchGroups = await loadWatchGroups(join(options.stateDirectory, "watch.json"), options.watch);
534
+ const listeners = new Set;
535
+ const controller = new AbortController;
536
+ const { signal } = controller;
537
+ let controllerBuild = null;
538
+ const emit = (message) => {
539
+ for (const listener of listeners)
540
+ listener(message);
541
+ };
542
+ const record = (entry) => {
543
+ timeline.add(entry);
544
+ emit({ event: "timeline", data: entry });
545
+ };
546
+ for (const entry of options.timeline ?? [])
547
+ timeline.add(entry);
548
+ const session = () => ({
549
+ build: {
550
+ manifest: manifest.build.id,
551
+ controller: controllerBuild,
552
+ matches: controllerBuild === manifest.build.id
553
+ },
554
+ connection: { ...adapter.connection(), endpoint: adapter.endpoint },
555
+ reading: { method: adapter.method, intervalMs: adapter.samplingMs },
556
+ tasks: telemetry.tasks(),
557
+ clock: telemetry.clock(),
558
+ ...options.release && { release: options.release },
559
+ ...options.projectRoot && { projectRoot: options.projectRoot }
560
+ });
561
+ const setBuild = (build) => {
562
+ if (build === controllerBuild)
563
+ return;
564
+ controllerBuild = build;
565
+ emit({ event: "session", data: session() });
566
+ };
567
+ const unavailable = (path, status) => ({
568
+ path,
569
+ value: null,
570
+ quality: "unavailable",
571
+ status,
572
+ receivedAt: new Date().toISOString()
573
+ });
574
+ const locate = (path) => {
575
+ const resolved = paths.resolve(path);
576
+ if (!resolved)
577
+ return unavailable(path, "Unknown path");
578
+ if (resolved.kind === "constant")
579
+ return {
580
+ path,
581
+ value: resolved.value,
582
+ quality: "good",
583
+ status: "Constant",
584
+ receivedAt: new Date().toISOString()
585
+ };
586
+ if (!resolved.readable)
587
+ return unavailable(path, "Not readable");
588
+ return resolved;
589
+ };
590
+ const targetRead = ({ target, type }) => ({
591
+ target,
592
+ type: paths.typeOf(type)
593
+ });
594
+ const sampleOf = (path, type, sample) => ({
595
+ path,
596
+ ...sample,
597
+ value: paths.decode(type, sample.value)
598
+ });
599
+ const watch = (patterns, listener) => {
600
+ const watched = new Map;
601
+ const reads = [];
602
+ for (const path of paths.expand(patterns)) {
603
+ const located = locate(path);
604
+ if (!("target" in located)) {
605
+ listener(located);
606
+ continue;
607
+ }
608
+ if (!watched.has(located.target))
609
+ reads.push(targetRead(located));
610
+ watched.set(located.target, [...watched.get(located.target) ?? [], { path, ...located }]);
611
+ }
612
+ if (!reads.length)
613
+ return () => {};
614
+ return adapter.subscribe(reads, (target, received) => {
615
+ for (const { path, type } of watched.get(target) ?? [])
616
+ listener(sampleOf(path, type, received));
617
+ });
618
+ };
619
+ const values = async (patterns) => {
620
+ const connection = adapter.connection();
621
+ if (connection.state !== "connected")
622
+ throw new DebugError(connection.error ?? "The controller is not connected", 503);
623
+ const located = paths.expand(patterns).map((path) => ({ path, located: locate(path) }));
624
+ const read = await adapter.read(located.flatMap(({ located }) => ("target" in located) ? [targetRead(located)] : []));
625
+ return located.map(({ path, located }) => {
626
+ if (!("target" in located))
627
+ return located;
628
+ const sample = read.get(located.target);
629
+ return sample ? sampleOf(path, located.type, sample) : unavailable(path, "Not read");
630
+ });
631
+ };
632
+ const stopBuild = adapter.subscribe([{ target: manifest.build.target, type: { kind: "scalar", iec: "STRING" } }], (_, sample) => {
633
+ if (sample.quality === "good" && typeof sample.value === "string")
634
+ setBuild(sample.value);
635
+ }, buildCheckMs);
636
+ const latest = new Map;
637
+ const traced = new Map;
638
+ const failures = new Map;
639
+ const requests = new Map;
640
+ let unread = new Set;
641
+ const shownJobs = createShown();
642
+ const shownHealth = createShown();
643
+ const shownDevices = createShown();
644
+ const sent = new Map;
645
+ const jobOf = (job) => jobState(job, {
646
+ value: (target) => latest.get(target),
647
+ traced: traced.get(job.id),
648
+ failure: failures.get(job.id),
649
+ unread: unread.has(job.id),
650
+ clock: telemetry.clock(),
651
+ now: Date.now(),
652
+ request: ({ job, service, sequence }) => requests.get(`${job}:${service}:${sequence}`)
653
+ });
654
+ const deviceOf = (id) => deviceState(manifest.devices.find((device) => device.id === id), manifest.io, (target) => latest.get(target));
655
+ const send = (key, message) => {
656
+ const json = JSON.stringify(message.data);
657
+ if (sent.get(key) === json)
658
+ return;
659
+ sent.set(key, json);
660
+ emit(message);
661
+ };
662
+ const updateJob = (job) => send(`job:${job.id}`, { event: "job", data: jobOf(job) });
663
+ const updateDevice = (id) => send(`device:${id}`, { event: "device", data: deviceOf(id) });
664
+ const reader = (intervalMs, update) => {
665
+ let current = { key: "", stop: () => {} };
666
+ return (reads) => {
667
+ const key = reads.map(({ read }) => read.target).join(`
668
+ `);
669
+ if (key === current.key)
670
+ return;
671
+ current.stop();
672
+ const owners = new Map;
673
+ for (const { owner, read } of reads)
674
+ owners.set(read.target, [...owners.get(read.target) ?? [], owner]);
675
+ current = {
676
+ key,
677
+ stop: reads.length ? adapter.subscribe([...new Map(reads.map(({ read }) => [read.target, read])).values()], (target, sample) => {
678
+ latest.set(target, sample);
679
+ for (const owner of owners.get(target) ?? [])
680
+ update(owner);
681
+ schedule();
682
+ }, intervalMs) : () => {}
683
+ };
684
+ };
685
+ };
686
+ const readJobs = reader(adapter.samplingMs, updateJob);
687
+ const readDevices = reader(hardwareMs, updateDevice);
688
+ const plan = () => {
689
+ const shown = manifest.jobs.filter((job) => shownJobs.has(job.id));
690
+ const busy = shown.filter((job) => busyOf(job, (target) => latest.get(target), traced.get(job.id)));
691
+ unread = new Set(busy.slice(detailedJobs).map(({ id }) => id));
692
+ readJobs([
693
+ ...shown.map((job) => ({ owner: job, read: busyRead(job) })),
694
+ ...busy.slice(0, detailedJobs).flatMap((job) => detailReads(job).map((read) => ({ owner: job, read })))
695
+ ]);
696
+ readDevices(manifest.devices.flatMap((device) => (shownDevices.has(device.id) ? deviceReads(device, manifest.io, paths.typeOf) : shownHealth.has(device.id) ? healthReads(device) : []).map((read) => ({ owner: device.id, read }))));
697
+ };
698
+ let planned = false;
699
+ const schedule = () => {
700
+ if (planned)
701
+ return;
702
+ planned = true;
703
+ queueMicrotask(() => {
704
+ planned = false;
705
+ plan();
706
+ });
707
+ };
708
+ const show = (interest) => {
709
+ const stops = [
710
+ shownJobs.add(interest.jobs),
711
+ shownHealth.add(interest.health),
712
+ shownDevices.add(interest.devices)
713
+ ];
714
+ schedule();
715
+ return () => {
716
+ for (const stop of stops)
717
+ stop();
718
+ schedule();
719
+ };
720
+ };
721
+ const receive = ({ build, task, sequence, at, record: received }) => {
722
+ if (received.kind === "trace") {
723
+ if (build !== manifest.build.id)
724
+ return;
725
+ const event = trace.decode(received, task, sequence);
726
+ if (event)
727
+ record(timelineEntries.trace(at, event));
728
+ const site = trace.job(received);
729
+ if (!site)
730
+ return;
731
+ const startedAt = site.started ? at : traced.get(site.job.id)?.startedAt;
732
+ traced.set(site.job.id, { busy: site.started, ...startedAt && { startedAt } });
733
+ updateJob(site.job);
734
+ schedule();
735
+ return;
736
+ }
737
+ record(timelineEntries.app(at, task, sequence, received));
738
+ if (received.kind !== "exception" || received.handled)
739
+ return;
740
+ const job = manifest.jobs.find(({ name, instance }) => name === received.function && instance === received.instance);
741
+ if (!job)
742
+ return;
743
+ failures.set(job.id, {
744
+ at,
745
+ code: received.code,
746
+ ...received.name && { name: received.name },
747
+ source: received.source
748
+ });
749
+ updateJob(job);
750
+ };
751
+ const pull = async (url, token) => {
752
+ const response = await fetch(url, { headers: { authorization: `Bearer ${token}` }, signal });
753
+ if (!response.ok)
754
+ throw new Error(`The server answered ${response.status}`);
755
+ return response.json();
756
+ };
757
+ const pullTelemetry = async ({ url, token }) => {
758
+ const reply = telemetryReply.parse(await pull(new URL(`/tsplc/telemetry?since=${telemetry.since()}`, url), token));
759
+ const received = telemetry.accept(reply);
760
+ for (const item of received)
761
+ receive(item);
762
+ const build = telemetry.build();
763
+ if (build)
764
+ setBuild(build);
765
+ if (reply.batches.length)
766
+ emit({ event: "session", data: session() });
767
+ };
768
+ const pullRequests = ({ url, token }) => {
769
+ let newest = 0;
770
+ const pending = new Map;
771
+ return async () => {
772
+ const since = pending.size ? Math.min(...pending.keys()) - 1 : newest;
773
+ const body = requestsReply.parse(await pull(new URL(`/tsplc/debug/requests?since=${since}`, url), token));
774
+ if (body.latest < newest) {
775
+ newest = 0;
776
+ pending.clear();
777
+ return;
778
+ }
779
+ const oldest = body.requests[0]?.seq;
780
+ if (oldest !== undefined) {
781
+ for (const seq of pending.keys())
782
+ if (seq < oldest)
783
+ pending.delete(seq);
784
+ }
785
+ for (const request of body.requests) {
786
+ const waiting = pending.get(request.seq);
787
+ if (waiting && request.status === "pending")
788
+ continue;
789
+ if (!waiting && request.seq <= newest)
790
+ continue;
791
+ const entry = waiting ?? timelineEntries.request(request);
792
+ entry.request = request;
793
+ newest = Math.max(newest, request.seq);
794
+ if (request.status === "pending")
795
+ pending.set(request.seq, entry);
796
+ else
797
+ pending.delete(request.seq);
798
+ if (!waiting)
799
+ timeline.add(entry);
800
+ emit({ event: "request", data: request });
801
+ emit({ event: "timeline", data: entry });
802
+ if (request.job === undefined || request.sequence === undefined)
803
+ continue;
804
+ requests.set(`${request.job}:${request.service}:${request.sequence}`, request);
805
+ for (const key of [...requests.keys()].slice(0, -requestsRemembered))
806
+ requests.delete(key);
807
+ const job = manifest.jobs.find(({ id }) => id === request.job);
808
+ if (job)
809
+ updateJob(job);
810
+ }
811
+ };
812
+ };
813
+ const stopConnection = adapter.onConnection((connection) => {
814
+ record(timelineEntries.connection(connection.since, connection.state, connection.error));
815
+ emit({ event: "session", data: session() });
816
+ if (connection.state !== "connected") {
817
+ for (const [target, sample] of latest)
818
+ latest.set(target, {
819
+ ...sample,
820
+ quality: "unavailable",
821
+ status: connection.error ?? "The controller is not connected"
822
+ });
823
+ for (const job of manifest.jobs)
824
+ updateJob(job);
825
+ for (const device of manifest.devices)
826
+ updateDevice(device.id);
827
+ }
828
+ });
829
+ const repeat = async (name, intervalMs, work) => {
830
+ let failure;
831
+ while (!signal.aborted) {
832
+ try {
833
+ await work();
834
+ failure = undefined;
835
+ } catch (error) {
836
+ if (signal.aborted)
837
+ return;
838
+ if (message(error) !== failure)
839
+ options.log(`${name} failed: ${message(error)}`);
840
+ failure = message(error);
841
+ }
842
+ await delay(intervalMs, undefined, { signal }).catch(() => {
843
+ return;
844
+ });
845
+ }
846
+ };
847
+ const { server } = options;
848
+ const running = server ? [
849
+ repeat("Reading the server's telemetry", pullMs, () => pullTelemetry(server)),
850
+ repeat("Reading the server's requests", pullMs, pullRequests(server))
851
+ ] : [];
852
+ return {
853
+ manifest,
854
+ paths,
855
+ session,
856
+ values,
857
+ jobs: () => manifest.jobs.map(jobOf),
858
+ hardware: () => manifest.devices.map(({ id }) => deviceOf(id)),
859
+ timeline: (since) => timeline.since(since),
860
+ watchGroups: () => watchGroups.groups(),
861
+ saveWatchGroups: (groups) => watchGroups.save(groups),
862
+ record,
863
+ watch,
864
+ show,
865
+ subscribe: (listener) => {
866
+ listeners.add(listener);
867
+ return () => listeners.delete(listener);
868
+ },
869
+ close: async () => {
870
+ controller.abort();
871
+ stopBuild();
872
+ readJobs([]);
873
+ readDevices([]);
874
+ stopConnection();
875
+ await Promise.all(running);
876
+ await adapter.close();
877
+ }
878
+ };
879
+ };
880
+ // packages/debug/src/http.ts
881
+ import { readFile as readFile2 } from "node:fs/promises";
882
+ import { z as z4 } from "zod";
883
+
884
+ // packages/debug/src/manifest.ts
885
+ import { z as z3 } from "zod";
886
+ var sourceRange = z3.object({
887
+ file: z3.number().int(),
888
+ line: z3.number().int(),
889
+ column: z3.number().int(),
890
+ endLine: z3.number().int(),
891
+ endColumn: z3.number().int()
892
+ });
893
+ var json = z3.lazy(() => z3.union([
894
+ z3.string(),
895
+ z3.number(),
896
+ z3.boolean(),
897
+ z3.null(),
898
+ z3.array(json),
899
+ z3.record(z3.string(), json)
900
+ ]));
901
+ var debugField = z3.object({
902
+ name: z3.string(),
903
+ target: z3.string(),
904
+ type: z3.string(),
905
+ declaration: sourceRange.optional(),
906
+ generated: z3.literal(true).optional()
907
+ });
908
+ var debugType = z3.discriminatedUnion("kind", [
909
+ z3.object({ kind: z3.literal("scalar"), iec: z3.string() }),
910
+ z3.object({
911
+ kind: z3.literal("enum"),
912
+ name: z3.string(),
913
+ members: z3.record(z3.string(), z3.number().int())
914
+ }),
915
+ z3.object({
916
+ kind: z3.literal("record"),
917
+ name: z3.string(),
918
+ declaration: sourceRange.optional(),
919
+ fields: z3.array(debugField)
920
+ }),
921
+ z3.object({
922
+ kind: z3.literal("array"),
923
+ element: z3.string(),
924
+ length: z3.number().int(),
925
+ lower: z3.number().int()
926
+ })
927
+ ]);
928
+ var debugRoot = z3.object({
929
+ path: z3.string(),
930
+ target: z3.string(),
931
+ type: z3.string(),
932
+ declaration: sourceRange.optional(),
933
+ readable: z3.boolean(),
934
+ generated: z3.literal(true).optional()
935
+ });
936
+ var debugConstant = z3.object({
937
+ path: z3.string(),
938
+ type: z3.string(),
939
+ value: json,
940
+ declaration: sourceRange.optional()
941
+ });
942
+ var debugIo = z3.object({
943
+ path: z3.string(),
944
+ target: z3.string(),
945
+ direction: z3.enum(["input", "output"]),
946
+ type: z3.string(),
947
+ device: z3.string().optional(),
948
+ channel: z3.string().optional(),
949
+ address: z3.string().optional(),
950
+ inverted: z3.literal(true).optional(),
951
+ shared: z3.literal(true).optional()
952
+ });
953
+ var debugChannel = z3.object({
954
+ id: z3.string(),
955
+ name: z3.string(),
956
+ direction: z3.enum(["input", "output"]),
957
+ type: z3.string(),
958
+ offsetBits: z3.number().int(),
959
+ bits: z3.number().int(),
960
+ address: z3.string().optional(),
961
+ unit: z3.string().optional(),
962
+ description: z3.string().optional()
963
+ });
964
+ var debugDevice = z3.object({
965
+ id: z3.string(),
966
+ product: z3.string().optional(),
967
+ vendor: z3.string().optional(),
968
+ productCode: z3.string().optional(),
969
+ network: z3.object({ address: z3.string(), rpiMs: z3.number().optional() }).optional(),
970
+ connection: z3.string().optional(),
971
+ assemblies: z3.array(z3.object({
972
+ direction: z3.enum(["input", "output"]),
973
+ bytes: z3.number().int(),
974
+ address: z3.string().optional(),
975
+ raw: z3.string().optional()
976
+ })),
977
+ health: z3.object({
978
+ state: z3.string().optional(),
979
+ states: z3.record(z3.string(), z3.number().int()).optional(),
980
+ diagnostic: z3.string().optional(),
981
+ message: z3.string().optional()
982
+ }).optional(),
983
+ status: z3.record(z3.string(), z3.string()).optional(),
984
+ channels: z3.array(debugChannel)
985
+ });
986
+ var debugFunction = z3.object({
987
+ id: z3.number().int(),
988
+ name: z3.string(),
989
+ declaration: sourceRange,
990
+ instance: z3.string().optional(),
991
+ task: z3.string().optional(),
992
+ target: z3.string(),
993
+ reads: z3.array(z3.object({ range: sourceRange, path: z3.string() }))
994
+ });
995
+ var debugJob = z3.object({
996
+ id: z3.number().int(),
997
+ name: z3.string(),
998
+ declaration: sourceRange,
999
+ instance: z3.string().optional(),
1000
+ target: z3.string(),
1001
+ steps: z3.array(z3.object({
1002
+ step: z3.number().int(),
1003
+ kind: z3.enum(["await", "return", "throw", "statement"]),
1004
+ range: sourceRange,
1005
+ text: z3.string()
1006
+ })),
1007
+ startedAt: z3.string().optional(),
1008
+ requests: z3.array(z3.object({
1009
+ service: z3.string(),
1010
+ target: z3.string(),
1011
+ sequence: z3.string(),
1012
+ job: z3.number().int().optional()
1013
+ })),
1014
+ generated: z3.literal(true).optional()
1015
+ });
1016
+ var debugEventSite = z3.object({
1017
+ id: z3.number().int(),
1018
+ kind: z3.enum(["state", "decision", "job"]),
1019
+ job: z3.number().int().optional(),
1020
+ phase: z3.enum(["started", "finished"]).optional(),
1021
+ path: z3.string(),
1022
+ function: z3.number().int().optional(),
1023
+ type: z3.string(),
1024
+ range: sourceRange,
1025
+ text: z3.string(),
1026
+ task: z3.string(),
1027
+ operands: z3.array(z3.object({
1028
+ text: z3.string(),
1029
+ range: sourceRange,
1030
+ type: z3.string(),
1031
+ path: z3.string().optional(),
1032
+ stable: z3.boolean()
1033
+ }))
1034
+ });
1035
+ var debugWriter = z3.object({
1036
+ output: z3.string(),
1037
+ target: z3.string(),
1038
+ sites: z3.array(z3.object({ id: z3.number().int(), range: sourceRange, function: z3.number().int() }))
1039
+ });
1040
+ var debugTask = z3.object({
1041
+ name: z3.string(),
1042
+ intervalMs: z3.number(),
1043
+ priority: z3.number().int()
1044
+ });
1045
+ var debugManifest = z3.object({
1046
+ kind: z3.literal("tsplc-debug"),
1047
+ version: z3.literal(1),
1048
+ build: z3.object({
1049
+ id: z3.string(),
1050
+ target: z3.string(),
1051
+ compiler: z3.string()
1052
+ }),
1053
+ sources: z3.array(z3.string()),
1054
+ types: z3.record(z3.string(), debugType),
1055
+ roots: z3.array(debugRoot),
1056
+ constants: z3.array(debugConstant),
1057
+ io: z3.array(debugIo),
1058
+ devices: z3.array(debugDevice),
1059
+ functions: z3.array(debugFunction),
1060
+ jobs: z3.array(debugJob),
1061
+ events: z3.array(debugEventSite),
1062
+ writers: z3.array(debugWriter),
1063
+ tasks: z3.array(debugTask),
1064
+ errors: z3.record(z3.string(), z3.string())
1065
+ });
1066
+
1067
+ // packages/debug/src/ui/index.ts
1068
+ import { createRequire } from "node:module";
1069
+ import { dirname as dirname2, resolve } from "node:path";
1070
+
1071
+ // packages/debug/src/ui/page.ts
1072
+ var css = `
1073
+ :root{color-scheme:dark;
1074
+ --bg:#0c0e11;--grid:rgba(255,255,255,.025);--panel:#13161b;--panel-2:#191d23;--line:#262b33;--line-soft:#1d2128;
1075
+ --fg:#d8dce2;--dim:#7c848f;--accent:#f2a33a;--link:#79c0ff;
1076
+ --live:#5ee0a0;--uncertain:#f3c64a;--bad:#ff6b6b;--stale:#8a919b;--unavailable:#5a616b;--constant:#b8a2ff;
1077
+ --hatch:rgba(138,145,155,.16);
1078
+ --mono:"JetBrains Mono","Cascadia Mono","IBM Plex Mono","SF Mono",Menlo,Consolas,monospace;
1079
+ --sans:"IBM Plex Sans","Segoe UI Variable Text","Segoe UI","Helvetica Neue",sans-serif}
1080
+ @media (prefers-color-scheme:light){:root{color-scheme:light;
1081
+ --bg:#f3f1ec;--grid:rgba(0,0,0,.035);--panel:#fffefb;--panel-2:#f6f4ef;--line:#d8d3c8;--line-soft:#e7e3da;
1082
+ --fg:#1b1e22;--dim:#6a6e75;--accent:#b5650a;--link:#0a5bb0;
1083
+ --live:#0d8048;--uncertain:#9a6400;--bad:#c42525;--stale:#767b83;--unavailable:#a1a6ad;--constant:#6447cc;
1084
+ --hatch:rgba(118,123,131,.16)}}
1085
+ *{box-sizing:border-box}
1086
+ html,body{height:100%}
1087
+ body{margin:0;font:13px/1.45 var(--sans);color:var(--fg);background:var(--bg);
1088
+ background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);
1089
+ background-size:24px 24px;display:grid;grid-template-rows:auto minmax(0,1fr);font-variant-numeric:tabular-nums}
1090
+ a{color:var(--link);text-decoration:none}a:hover{text-decoration:underline}
1091
+ code,.mono,.path,.src{font-family:var(--mono);font-size:12px}
1092
+ .src,.nowrap,.grid td.num{white-space:nowrap}
1093
+ code{color:var(--fg);background:var(--panel-2);border:1px solid var(--line-soft);border-radius:3px;padding:0 4px}
1094
+ .dim,.hint,.empty{color:var(--dim)}.hint{margin:4px 0 12px}.empty{margin:8px 0;font-style:italic}
1095
+ .err{color:var(--bad)}.num{text-align:right}
1096
+ button,input{font:inherit;color:inherit}
1097
+ button{background:var(--panel-2);border:1px solid var(--line);border-radius:4px;padding:3px 10px;cursor:pointer}
1098
+ button:hover{border-color:var(--dim)}button.primary{background:var(--accent);border-color:var(--accent);color:#16110a;font-weight:600}
1099
+ button:disabled{opacity:.5;cursor:default}
1100
+ input{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:4px 8px}
1101
+ input:focus,button:focus-visible,.vlist:focus-visible{outline:1px solid var(--accent);outline-offset:1px}
1102
+ h1{font:600 18px var(--sans);margin:0 0 12px;letter-spacing:-.01em}
1103
+ h2{font:600 10.5px var(--sans);text-transform:uppercase;letter-spacing:.14em;color:var(--dim);margin:0 0 10px}
1104
+ h2 a{color:inherit}h3{font:600 12px var(--sans);margin:14px 0 6px}
1105
+
1106
+ header{background:var(--panel);border-top:2px solid var(--accent);border-bottom:1px solid var(--line)}
1107
+ .bar{display:flex;align-items:stretch;gap:0;overflow-x:auto}
1108
+ .brand{display:flex;align-items:center;gap:6px;padding:8px 16px;border-right:1px solid var(--line);white-space:nowrap}
1109
+ .brand b{font:700 13px var(--mono);letter-spacing:.04em}.brand span{font:500 13px var(--mono);color:var(--accent)}
1110
+ .brand .ro{font:600 9.5px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);border-radius:3px;padding:1px 5px;margin-left:6px}
1111
+ .readouts{display:flex;flex:1}
1112
+ .readout{display:flex;flex-direction:column;justify-content:center;padding:6px 14px;border-right:1px solid var(--line-soft);white-space:nowrap}
1113
+ .readout .label{font:600 9px var(--sans);letter-spacing:.16em;text-transform:uppercase;color:var(--dim)}
1114
+ .readout .reading{font:12px var(--mono);display:flex;align-items:center;gap:8px}
1115
+ .build.match i{color:var(--live);font-style:normal}.build.mismatch{color:var(--bad)}.build.mismatch i{font-style:normal;font-weight:700}
1116
+ .task b{color:var(--fg);font-weight:600}
1117
+ .dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--unavailable)}
1118
+ .dot.c-connected{background:var(--live);box-shadow:0 0 6px var(--live)}.dot.c-lost,.dot.c-failed{background:var(--bad)}.dot.c-unavailable{background:var(--uncertain)}
1119
+ .dot.c-connecting{background:var(--uncertain);animation:pulse 1s infinite alternate}
1120
+ @keyframes pulse{to{opacity:.3}}
1121
+ nav{display:flex;gap:2px;padding:0 10px;border-top:1px solid var(--line-soft)}
1122
+ nav a{color:var(--dim);padding:7px 12px 6px;text-transform:capitalize;border-bottom:2px solid transparent;font-weight:500}
1123
+ nav a:hover{color:var(--fg);text-decoration:none}nav a.on{color:var(--fg);border-bottom-color:var(--accent)}
1124
+ .banner{padding:7px 16px;font-weight:600;display:flex;gap:12px;align-items:center}
1125
+ .banner.danger{background:var(--bad);color:#fff}
1126
+ .banner.warn{background:repeating-linear-gradient(135deg,var(--uncertain) 0 14px,color-mix(in srgb,var(--uncertain) 85%,#000) 14px 28px);color:#1b1404}
1127
+ .banner.error{background:var(--panel-2);color:var(--bad);border-top:1px solid var(--bad)}
1128
+ .banner button{margin-left:auto}
1129
+
1130
+ main{overflow:auto;min-height:0}
1131
+ .page{padding:16px 18px 28px;animation:enter .18s ease-out}
1132
+ @keyframes enter{from{opacity:0;transform:translateY(3px)}}
1133
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:12px 14px}
1134
+ .toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:10px}
1135
+ .check{display:inline-flex;gap:5px;align-items:center;color:var(--dim);cursor:pointer}.check input{margin:0}
1136
+ .sep{margin-left:8px}.count{color:var(--dim);font:12px var(--mono);margin-left:auto}
1137
+ .tabs{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px;align-items:center}
1138
+ .tab{background:transparent}.tab.on{border-color:var(--accent);color:var(--fg)}
1139
+ .tab .n{color:var(--dim);margin-left:6px;font:11px var(--mono)}
1140
+ .tag{font:600 9.5px var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);border-radius:3px;padding:0 4px;margin:0 4px}
1141
+ dl{display:grid;grid-template-columns:max-content 1fr;gap:3px 14px;margin:0}dt{color:var(--dim)}dd{margin:0;min-width:0;overflow-wrap:anywhere}
1142
+ .facts{margin-bottom:14px}.readings dd{display:flex;gap:8px;align-items:baseline}
1143
+ .crumbs{color:var(--dim);margin-bottom:4px}
1144
+
1145
+ table.grid{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:6px}
1146
+ .grid th{font:600 9.5px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--dim);text-align:left;padding:7px 10px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel)}
1147
+ .grid td{padding:6px 10px;border-bottom:1px solid var(--line-soft);vertical-align:top}
1148
+ .grid tr:last-child td{border-bottom:0}.link-row{cursor:pointer}.link-row:hover td{background:var(--panel-2)}
1149
+ tr.highlight td{background:color-mix(in srgb,var(--accent) 12%,transparent)}
1150
+
1151
+ .value{font:12.5px var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1152
+ .meta{font:11px var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1153
+ .meta::before{display:inline-block;width:1.4ch}
1154
+ .s-live{color:var(--live)}.meta.s-live::before{content:"●"}
1155
+ .s-uncertain{color:var(--uncertain)}.value.s-uncertain{text-decoration:underline dotted}.meta.s-uncertain::before{content:"◐"}
1156
+ .s-bad{color:var(--bad)}.value.s-bad{text-decoration:line-through}.meta.s-bad::before{content:"✕"}
1157
+ .s-stale{color:var(--stale)}.value.s-stale{background:repeating-linear-gradient(135deg,transparent 0 4px,var(--hatch) 4px 8px)}.meta.s-stale::before{content:"○"}
1158
+ .s-unavailable{color:var(--unavailable)}.meta.s-unavailable::before{content:"–"}
1159
+ .s-constant{color:var(--constant)}.value.s-constant{font-style:italic}.meta.s-constant::before{content:"◆"}
1160
+ .s-group{color:var(--dim)}
1161
+
1162
+ .variables{height:100%;display:flex;flex-direction:column;padding-bottom:12px}
1163
+ .search{flex:1;min-width:280px;max-width:640px;font-family:var(--mono);font-size:12.5px;padding:6px 10px}
1164
+ .split{flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:12px}
1165
+ .listwrap{display:flex;flex-direction:column;min-height:0;background:var(--panel);border:1px solid var(--line);border-radius:6px;overflow:hidden}
1166
+ .vlist{flex:1;overflow:auto;position:relative;outline:none}
1167
+ .vspacer{position:relative}.vwindow{position:absolute;left:0;right:0;top:0}
1168
+ .row{display:grid;grid-template-columns:minmax(220px,1fr) minmax(90px,15ch) 21ch 17ch 40px minmax(110px,19ch);gap:8px;align-items:center;height:26px;padding-right:10px;border-bottom:1px solid var(--line-soft);cursor:default}
1169
+ .row:hover{background:var(--panel-2)}.row.selected{background:color-mix(in srgb,var(--accent) 13%,transparent);box-shadow:inset 2px 0 0 var(--accent)}
1170
+ .row.head{font:600 9.5px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--dim);border-bottom:1px solid var(--line);padding-left:10px}
1171
+ .row.generated .name{opacity:.7}
1172
+ .name{font:12.5px var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center}
1173
+ .name mark{background:color-mix(in srgb,var(--accent) 35%,transparent);color:inherit;border-radius:2px}
1174
+ .twisty{display:inline-grid;place-items:center;width:18px;height:18px;flex:none;padding:0;border:0;background:none;color:var(--dim);font-size:10px}
1175
+ .type{font:11.5px var(--mono);color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1176
+ .row .src{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px}
1177
+ .rec{padding:0;width:22px;height:18px;border:1px solid transparent;background:none;color:var(--line);font-size:11px}
1178
+ .rec:hover{color:var(--dim);border-color:var(--line)}.rec.on{color:var(--bad)}.rec.elsewhere{color:var(--dim)}
1179
+ .side{display:flex;flex-direction:column;gap:12px;overflow:auto;min-height:0}
1180
+ .detail-path{font-size:12.5px;overflow-wrap:anywhere;margin-bottom:8px;color:var(--fg)}
1181
+ .detail-value{display:flex;gap:12px;align-items:baseline;margin-bottom:10px;padding:8px 10px;background:var(--panel-2);border-radius:4px}
1182
+ .detail-value .value{font-size:20px}.detail .site{margin:3px 0}.detail button{margin-top:10px}
1183
+ .member{display:grid;grid-template-columns:minmax(0,1fr) auto auto 22px;gap:8px;align-items:center;padding:3px 0;border-bottom:1px solid var(--line-soft)}
1184
+ .member .path{font-size:11.5px}.remove{padding:0 5px;border:0;background:none;color:var(--dim)}
1185
+ .groups form input{width:110px;padding:2px 6px}
1186
+
1187
+ .badge{display:inline-block;font:600 10.5px var(--mono);letter-spacing:.04em;padding:1px 7px;border-radius:3px;border:1px solid;text-transform:uppercase}
1188
+ .h-ok{color:var(--live);border-color:var(--live)}
1189
+ .h-fault{color:#fff;background:var(--bad);border-color:var(--bad)}
1190
+ .h-stale{color:var(--stale);border-color:var(--stale);background:repeating-linear-gradient(135deg,transparent 0 4px,var(--hatch) 4px 8px)}
1191
+ .h-unknown{color:var(--dim);border-color:var(--dim);border-style:dashed}
1192
+ .st{font:600 11px var(--mono);text-transform:lowercase}
1193
+ .st-busy,.st-pending,.st-connecting,.st-cancelling{color:var(--uncertain)}
1194
+ .st-failed,.st-error,.st-timeout,.st-lost{color:var(--bad)}.st-unavailable{color:var(--uncertain)}
1195
+ .st-done,.st-ok,.st-connected{color:var(--live)}.st-idle,.st-unknown{color:var(--dim)}
1196
+ .await code{white-space:pre-wrap}
1197
+
1198
+ .overview{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-items:start}
1199
+ .overview .wide{grid-column:1/-1}
1200
+ .overview>*{animation:enter .25s ease-out backwards}.overview>:nth-child(2){animation-delay:.04s}.overview>:nth-child(3){animation-delay:.08s}.overview>:nth-child(4){animation-delay:.12s}
1201
+ .counts{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:10px}
1202
+ .count-box{border:1px solid;border-radius:5px;padding:6px 10px;display:flex;flex-direction:column}
1203
+ .count-box b{font:600 22px var(--mono)}.count-box span{font:600 9.5px var(--sans);letter-spacing:.14em;text-transform:uppercase}
1204
+ .count-box.h-fault{background:color-mix(in srgb,var(--bad) 12%,transparent);color:var(--bad)}
1205
+ .device-line{padding:3px 0}
1206
+
1207
+ .assemblies{display:flex;flex-direction:column;gap:12px;margin-bottom:18px}
1208
+ .assembly h3{margin:0 0 10px}
1209
+ .bitgrid{display:grid;grid-template-columns:4ch repeat(8,52px) 4ch minmax(0,1fr);gap:3px;align-items:stretch}
1210
+ .bh{font:600 9.5px var(--mono);color:var(--dim);text-align:center;letter-spacing:.06em}.legend-head{text-align:left;padding-left:6px}
1211
+ .byte,.hex{font-size:11.5px;color:var(--dim);align-self:center}
1212
+ .bit{position:relative;height:34px;display:flex;flex-direction:column;align-items:center;justify-content:center;font:600 13px var(--mono);
1213
+ border:1px dashed var(--line);border-radius:3px;color:var(--unavailable)}
1214
+ .bit small{font:9px var(--mono);color:var(--dim);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 2px}
1215
+ .bit.mapped{border-style:solid;color:var(--dim);background:var(--panel-2)}
1216
+ .bit.on{color:var(--fg)}
1217
+ .bit.mapped.on{color:var(--live);background:color-mix(in srgb,var(--live) 16%,transparent);border-color:color-mix(in srgb,var(--live) 60%,transparent);box-shadow:0 0 8px color-mix(in srgb,var(--live) 25%,transparent)}
1218
+ .bit.none{color:var(--unavailable)}
1219
+ .q-stale .bit,.q-uncertain .bit,.q-bad .bit{background-image:repeating-linear-gradient(135deg,transparent 0 4px,var(--hatch) 4px 8px)}
1220
+ .q-stale .bit.on,.q-bad .bit.on{color:var(--stale);box-shadow:none;border-color:var(--line)}
1221
+ .legend{display:flex;flex-wrap:wrap;gap:4px 12px;align-items:center;font-size:11.5px;padding-left:6px}
1222
+ .legend-item{display:inline-flex;gap:6px;align-items:baseline}.legend-item b{font:600 11px var(--mono)}
1223
+ .mapped-path{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:baseline}
1224
+
1225
+ .recent .entries{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:6px}
1226
+ .entry{display:grid;grid-template-columns:14ch 9ch minmax(0,1fr);gap:10px;padding:7px 12px;border-bottom:1px solid var(--line-soft)}
1227
+ .overview .entry{padding:6px 0}
1228
+ .when{color:var(--dim);font-size:11.5px}.approx{color:var(--uncertain);margin-left:4px;cursor:help}
1229
+ .source{font:600 9.5px var(--sans);letter-spacing:.12em;text-transform:uppercase;padding-top:2px}
1230
+ .src-plc{color:var(--accent)}.src-server{color:var(--link)}.src-agent{color:var(--constant)}.src-debugger{color:var(--dim)}
1231
+ .body>div{margin:1px 0}.val{font:600 12.5px var(--mono)}
1232
+ .chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-top:3px}
1233
+ .chip{display:inline-flex;align-items:baseline;gap:3px;font:11.5px var(--mono);border:1px solid var(--line);border-radius:10px;padding:0 8px;background:var(--panel-2)}
1234
+ .chip.unstable{border-style:dashed}
1235
+ .tag.lvl-info{color:var(--link);border-color:var(--link)}.tag.lvl-warn{color:var(--uncertain);border-color:var(--uncertain)}.tag.lvl-error{color:var(--bad);border-color:var(--bad)}
1236
+ .recent>button{margin-top:10px}
1237
+
1238
+ .fatal{padding:40px;max-width:720px}.fatal h1{color:var(--bad)}
1239
+ @media (max-width:1100px){.split{grid-template-columns:minmax(0,1fr)}.side{max-height:40vh}.overview{grid-template-columns:minmax(0,1fr)}}
1240
+ `;
1241
+ var html = `<!doctype html>
1242
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1243
+ <title>tsPLC debug</title><link rel="icon" href="data:,">
1244
+ <script>if(!location.pathname.endsWith("/"))location.replace(location.pathname+"/"+location.search+location.hash)</script>
1245
+ <style>${css}</style></head>
1246
+ <body><p class="empty" style="padding:24px">Loading the debugger…</p>
1247
+ <script type="module" src="./ui.js"></script></body></html>`;
1248
+
1249
+ // packages/debug/src/ui/index.ts
1250
+ var ui;
1251
+ var build = async () => {
1252
+ const result = await Bun.build({
1253
+ entrypoints: [
1254
+ resolve(dirname2(createRequire(import.meta.url).resolve("@tsplc/debug")), "../src/ui/main.ts")
1255
+ ],
1256
+ target: "browser",
1257
+ minify: true
1258
+ });
1259
+ if (!result.success)
1260
+ throw new AggregateError(result.logs, "Bundling the debugger UI failed");
1261
+ return { html, script: await result.outputs[0].text() };
1262
+ };
1263
+ var loadUi = () => ui ??= build();
1264
+
1265
+ // packages/debug/src/http.ts
1266
+ var heartbeatMs = 5000;
1267
+ var encoder = new TextEncoder;
1268
+ var json2 = (body, status = 200) => Response.json(body, { status, headers: { "Cache-Control": "no-store" } });
1269
+ var list = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1270
+ var loadManifest = async (file) => debugManifest.parse(JSON.parse(await readFile2(file, "utf8")));
1271
+ var interest = (search) => {
1272
+ const jobs = list(search.get("jobs")).map(Number);
1273
+ if (!jobs.every(Number.isInteger))
1274
+ throw new DebugError("jobs must be job ids", 400);
1275
+ return {
1276
+ watch: list(search.get("watch")),
1277
+ jobs,
1278
+ health: list(search.get("health")),
1279
+ devices: list(search.get("devices"))
1280
+ };
1281
+ };
1282
+ var stream = (debug, { watch, ...shown }) => {
1283
+ debug.paths.expand(watch);
1284
+ let stop = () => {};
1285
+ return new Response(new ReadableStream({
1286
+ start: (controller) => {
1287
+ let open = true;
1288
+ const write = (text) => {
1289
+ if (open)
1290
+ controller.enqueue(encoder.encode(text));
1291
+ };
1292
+ const send = ({ event, data }) => write(`event: ${event}
1293
+ data: ${JSON.stringify(data)}
1294
+
1295
+ `);
1296
+ send({ event: "session", data: debug.session() });
1297
+ const latest = new Map;
1298
+ const sendSample = (sample) => {
1299
+ latest.set(sample.path, { sample, sentAt: Date.now() });
1300
+ send({ event: "sample", data: sample });
1301
+ };
1302
+ const unsubscribe = debug.subscribe(send);
1303
+ const unwatch = debug.watch(watch, sendSample);
1304
+ const unshow = debug.show(shown);
1305
+ const heartbeat = setInterval(() => write(`: keepalive
1306
+
1307
+ `), heartbeatMs);
1308
+ const { intervalMs } = debug.session().reading;
1309
+ const refresh = setInterval(() => {
1310
+ if (debug.session().connection.state !== "connected")
1311
+ return;
1312
+ const now = Date.now();
1313
+ for (const { sample, sentAt } of latest.values())
1314
+ if (now - sentAt >= intervalMs)
1315
+ sendSample({ ...sample, receivedAt: new Date(now).toISOString() });
1316
+ }, intervalMs);
1317
+ stop = () => {
1318
+ open = false;
1319
+ clearInterval(heartbeat);
1320
+ clearInterval(refresh);
1321
+ unsubscribe();
1322
+ unwatch();
1323
+ unshow();
1324
+ };
1325
+ },
1326
+ cancel: () => stop()
1327
+ }), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-store" } });
1328
+ };
1329
+ var createDebugHandler = (debug, log) => async (request, route) => {
1330
+ const { searchParams } = new URL(request.url);
1331
+ const key = `${request.method} ${route}`;
1332
+ try {
1333
+ switch (key) {
1334
+ case "GET /":
1335
+ return new Response((await loadUi()).html, {
1336
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1337
+ });
1338
+ case "GET /ui.js":
1339
+ return new Response((await loadUi()).script, {
1340
+ headers: { "Content-Type": "text/javascript; charset=utf-8" }
1341
+ });
1342
+ case "GET /api/session":
1343
+ return json2(debug.session());
1344
+ case "GET /api/manifest":
1345
+ return json2(debug.manifest);
1346
+ case "GET /api/stream":
1347
+ return stream(debug, interest(searchParams));
1348
+ case "GET /api/values":
1349
+ return json2(await debug.values(list(searchParams.get("paths"))));
1350
+ case "GET /api/jobs":
1351
+ return json2(debug.jobs());
1352
+ case "GET /api/hardware":
1353
+ return json2(debug.hardware());
1354
+ case "GET /api/timeline":
1355
+ return json2(debug.timeline(searchParams.get("since") ?? undefined));
1356
+ case "GET /api/watch":
1357
+ return json2(debug.watchGroups());
1358
+ case "PUT /api/watch":
1359
+ return json2(await debug.saveWatchGroups(watchGroupsSchema.parse(await request.json())));
1360
+ }
1361
+ return json2({ error: `No route ${key}` }, 404);
1362
+ } catch (error) {
1363
+ if (error instanceof DebugError)
1364
+ return json2({ error: error.message }, error.status);
1365
+ if (error instanceof RangeError || error instanceof SyntaxError)
1366
+ return json2({ error: error.message }, 400);
1367
+ if (error instanceof z4.ZodError)
1368
+ return json2({ error: z4.prettifyError(error) }, 400);
1369
+ log(`${key} failed: ${error instanceof Error ? error.stack : String(error)}`);
1370
+ return json2({ error: "The debugger failed; see its log" }, 500);
1371
+ }
1372
+ };
1373
+ var serveDebugger = (debug, options) => {
1374
+ const handle = createDebugHandler(debug, options.log);
1375
+ return Bun.serve({
1376
+ hostname: "127.0.0.1",
1377
+ port: options.port,
1378
+ fetch: (request) => {
1379
+ const host = request.headers.get("host")?.replace(/:\d+$/, "");
1380
+ if (host !== "127.0.0.1" && host !== "localhost")
1381
+ return json2({ error: "Use http://127.0.0.1 or http://localhost" }, 403);
1382
+ return handle(request, new URL(request.url).pathname);
1383
+ }
1384
+ });
1385
+ };
1386
+ // packages/debug/src/login.ts
1387
+ import { setTimeout as delay2 } from "node:timers/promises";
1388
+
1389
+ class LoginUnavailable extends Error {
1390
+ constructor(message, options) {
1391
+ super(message, options);
1392
+ this.state = options?.state ?? "unavailable";
1393
+ }
1394
+ state;
1395
+ }
1396
+ var noLogin = "Live values need an engineering login: run `tsplc login`.";
1397
+ var retryMs = 5000;
1398
+ var integerTypes = new Set([
1399
+ ...["SINT", "INT", "DINT", "LINT", "USINT", "UINT", "UDINT", "ULINT"],
1400
+ ...["BYTE", "WORD", "DWORD", "LWORD"]
1401
+ ]);
1402
+ var timeUnits = {
1403
+ d: 86400000,
1404
+ h: 3600000,
1405
+ m: 60000,
1406
+ s: 1000,
1407
+ ms: 1,
1408
+ us: 0.001,
1409
+ ns: 0.000001
1410
+ };
1411
+ var typeName = (type) => type.kind === "scalar" ? type.iec : type.kind;
1412
+ var message2 = (error) => error instanceof Error ? error.message : String(error);
1413
+ var integer = (text) => {
1414
+ const radix = /^(2|8|16)#([0-9a-f_]+)$/i.exec(text);
1415
+ const digits = radix ? radix[2].replaceAll("_", "") : text.replaceAll("_", "");
1416
+ if (!radix && !/^[+-]?\d+$/.test(digits))
1417
+ return;
1418
+ const value = BigInt(radix ? `0${{ 2: "b", 8: "o", 16: "x" }[radix[1]]}${digits}` : digits);
1419
+ return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
1420
+ };
1421
+ var duration = (text) => {
1422
+ const body = /^L?T(?:IME)?#(-?)(.+)$/i.exec(text);
1423
+ if (!body)
1424
+ return;
1425
+ const parts = [...body[2].replaceAll("_", "").matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|d|h|m|s)/gi)];
1426
+ if (parts.map(([part]) => part).join("") !== body[2].replaceAll("_", ""))
1427
+ return;
1428
+ const ms = parts.reduce((total, [, amount, unit]) => total + Number(amount) * timeUnits[unit.toLowerCase()], 0);
1429
+ return body[1] ? -ms : ms;
1430
+ };
1431
+ var quoted = (text) => {
1432
+ const body = /^(['"])(.*)\1$/s.exec(text);
1433
+ if (!body)
1434
+ return;
1435
+ return body[2].replace(/\$([0-9a-f]{2}|[$'"lnprt])/gi, (_, code) => {
1436
+ if (code.length === 2)
1437
+ return String.fromCharCode(parseInt(code, 16));
1438
+ const named = { l: `
1439
+ `, n: `
1440
+ `, p: "\f", r: "\r", t: "\t" };
1441
+ return named[code.toLowerCase()] ?? code;
1442
+ });
1443
+ };
1444
+ var parseLoginValue = (type, text) => {
1445
+ const trimmed = text.trim();
1446
+ const untyped = trimmed.replace(/^(?!L?T(?:IME)?#)[a-z_]+#(?=.)/i, "");
1447
+ switch (type.kind) {
1448
+ case "enum": {
1449
+ const number = integer(untyped);
1450
+ if (typeof number === "number")
1451
+ return number;
1452
+ const named = /^(?:\w+\.)*(\w+)(?:\s*\((-?\d+)\))?$/.exec(untyped);
1453
+ if (!named)
1454
+ return;
1455
+ return type.members[named[1]] ?? (named[2] === undefined ? named[1] : Number(named[2]));
1456
+ }
1457
+ case "array": {
1458
+ const items = /^\[?(.*?)\]?$/s.exec(untyped)[1].trim();
1459
+ if (!items)
1460
+ return [];
1461
+ const element = { kind: "scalar", iec: type.element };
1462
+ const values = items.split(/\s*,\s*/).map((item) => parseLoginValue(element, item));
1463
+ return values.length === type.length && values.every((value) => value !== undefined) ? values : undefined;
1464
+ }
1465
+ case "record":
1466
+ return;
1467
+ case "scalar":
1468
+ break;
1469
+ }
1470
+ const iec = type.iec.toUpperCase();
1471
+ if (iec === "BOOL")
1472
+ return { TRUE: true, FALSE: false, 1: true, 0: false }[untyped.toUpperCase()];
1473
+ if (integerTypes.has(iec))
1474
+ return integer(untyped);
1475
+ if (iec === "REAL" || iec === "LREAL") {
1476
+ const number = Number(untyped);
1477
+ return untyped !== "" && !Number.isNaN(number) ? number : undefined;
1478
+ }
1479
+ if (iec === "TIME" || iec === "LTIME")
1480
+ return duration(trimmed);
1481
+ if (/^W?STRING\b/.test(iec))
1482
+ return quoted(trimmed) ?? trimmed;
1483
+ return trimmed;
1484
+ };
1485
+ var createLoginAdapter = (options) => {
1486
+ const samplingMs = options.samplingMs ?? 1000;
1487
+ let connection = { state: "connecting", since: new Date().toISOString() };
1488
+ const connectionListeners = new Set;
1489
+ const subscriptions = new Set;
1490
+ const rejected = new Map;
1491
+ const controller = new AbortController;
1492
+ const setConnection = (state, error) => {
1493
+ if (controller.signal.aborted)
1494
+ return;
1495
+ if (connection.state === state && connection.error === error)
1496
+ return;
1497
+ if (state !== "connected")
1498
+ rejected.clear();
1499
+ connection = { state, since: new Date().toISOString(), ...error && { error } };
1500
+ for (const listener of connectionListeners)
1501
+ listener(connection);
1502
+ };
1503
+ const split = async (expressions) => {
1504
+ try {
1505
+ return await options.read(expressions);
1506
+ } catch (error) {
1507
+ if (error instanceof LoginUnavailable)
1508
+ throw error;
1509
+ if (expressions.length === 1) {
1510
+ rejected.set(expressions[0], message2(error));
1511
+ return {};
1512
+ }
1513
+ const half = Math.ceil(expressions.length / 2);
1514
+ return {
1515
+ ...await split(expressions.slice(0, half)),
1516
+ ...await split(expressions.slice(half))
1517
+ };
1518
+ }
1519
+ };
1520
+ const readText = async (expressions) => {
1521
+ try {
1522
+ return await options.read(expressions);
1523
+ } catch (error) {
1524
+ if (error instanceof LoginUnavailable)
1525
+ throw error;
1526
+ await options.read([options.probe]);
1527
+ return split(expressions);
1528
+ }
1529
+ };
1530
+ const read = async (targets) => {
1531
+ const unique = [...new Map(targets.map((target) => [target.target, target])).values()];
1532
+ const expressions = unique.map(({ target }) => target).filter((target) => !rejected.has(target));
1533
+ const samples = new Map;
1534
+ let values;
1535
+ const overdue = setTimeout(() => setConnection("lost", "The engineering login stopped answering"), 3 * samplingMs);
1536
+ try {
1537
+ values = expressions.length ? await readText(expressions) : {};
1538
+ setConnection("connected");
1539
+ } catch (error) {
1540
+ setConnection(error instanceof LoginUnavailable ? error.state : "lost", message2(error));
1541
+ const receivedAt = new Date().toISOString();
1542
+ for (const { target } of unique)
1543
+ samples.set(target, {
1544
+ value: null,
1545
+ quality: "unavailable",
1546
+ status: message2(error),
1547
+ receivedAt
1548
+ });
1549
+ return samples;
1550
+ } finally {
1551
+ clearTimeout(overdue);
1552
+ }
1553
+ const receivedAt = new Date().toISOString();
1554
+ for (const { target, type } of unique) {
1555
+ const text = values[target];
1556
+ const refused = rejected.get(target);
1557
+ const value = text === undefined ? undefined : parseLoginValue(type, text);
1558
+ samples.set(target, value !== undefined ? { value, quality: "good", status: "Good", receivedAt } : {
1559
+ value: text ?? null,
1560
+ quality: "bad",
1561
+ status: refused ?? (text === undefined ? "Not returned by the login" : `Not a ${typeName(type)}`),
1562
+ receivedAt
1563
+ });
1564
+ }
1565
+ return samples;
1566
+ };
1567
+ const poll = async () => {
1568
+ while (!controller.signal.aborted) {
1569
+ const started = Date.now();
1570
+ const due = [...subscriptions].filter(({ due }) => due <= started + samplingMs / 2);
1571
+ if (due.length) {
1572
+ for (const subscription of due)
1573
+ subscription.due = started + subscription.intervalMs;
1574
+ const stopped = Promise.withResolvers();
1575
+ const stop = () => stopped.resolve();
1576
+ controller.signal.addEventListener("abort", stop, { once: true });
1577
+ const samples = await Promise.race([
1578
+ read(due.flatMap(({ targets }) => targets)),
1579
+ stopped.promise
1580
+ ]).finally(() => controller.signal.removeEventListener("abort", stop));
1581
+ if (!samples || controller.signal.aborted)
1582
+ break;
1583
+ for (const subscription of due)
1584
+ if (subscriptions.has(subscription))
1585
+ for (const { target } of subscription.targets)
1586
+ subscription.listener(target, samples.get(target));
1587
+ }
1588
+ const took = Date.now() - started;
1589
+ const waiting = connection.state !== "connected" && connection.state !== "connecting";
1590
+ await delay2(waiting ? retryMs : took > samplingMs ? took : samplingMs - took, undefined, {
1591
+ signal: controller.signal
1592
+ }).catch(() => {
1593
+ return;
1594
+ });
1595
+ }
1596
+ };
1597
+ const running = poll();
1598
+ return {
1599
+ method: "engineering login",
1600
+ endpoint: options.endpoint,
1601
+ samplingMs,
1602
+ connection: () => connection,
1603
+ onConnection: (listener) => {
1604
+ connectionListeners.add(listener);
1605
+ return () => connectionListeners.delete(listener);
1606
+ },
1607
+ read,
1608
+ subscribe: (targets, listener, intervalMs = samplingMs) => {
1609
+ const subscription = { targets, listener, intervalMs, due: 0 };
1610
+ subscriptions.add(subscription);
1611
+ return () => subscriptions.delete(subscription);
1612
+ },
1613
+ close: async () => {
1614
+ controller.abort();
1615
+ subscriptions.clear();
1616
+ connectionListeners.clear();
1617
+ await running;
1618
+ }
1619
+ };
1620
+ };
1621
+ // packages/debug/src/opcua.ts
1622
+ import { resolve as resolve2 } from "node:path";
1623
+ import { createMonitor } from "@tsplc/opcua";
1624
+ var opcUaConnection = (monitor, options) => {
1625
+ const password = monitor.passwordEnv && options.env[monitor.passwordEnv];
1626
+ if (monitor.username && !password)
1627
+ throw new Error(monitor.passwordEnv ? `Set ${monitor.passwordEnv} to the OPC UA password of ${monitor.username}.` : "Configure monitor.passwordEnv for the OPC UA user.");
1628
+ return {
1629
+ endpoint: monitor.endpoint,
1630
+ namespaceUri: monitor.namespaceUri,
1631
+ certificateDirectory: resolve2(options.root, monitor.certificateDirectory),
1632
+ security: monitor.securityPolicy,
1633
+ securityMode: monitor.securityMode === "Sign" ? "Sign" : "SignAndEncrypt",
1634
+ ...monitor.username && password && { credentials: { username: monitor.username, password } }
1635
+ };
1636
+ };
1637
+ var createOpcUaAdapter = (options) => {
1638
+ const monitor = createMonitor(options);
1639
+ const controller = new AbortController;
1640
+ const running = monitor.run(controller.signal);
1641
+ const sample = ({ value, quality, status }) => ({
1642
+ value,
1643
+ quality,
1644
+ status,
1645
+ receivedAt: new Date().toISOString()
1646
+ });
1647
+ const names = (targets) => targets.map(({ target }) => target);
1648
+ return {
1649
+ method: "OPC UA",
1650
+ endpoint: options.endpoint,
1651
+ samplingMs: monitor.samplingMs,
1652
+ connection: monitor.status,
1653
+ onConnection: monitor.onStatus,
1654
+ read: async (targets) => new Map(Object.entries(await monitor.read(names(targets))).map(([target, reading]) => [
1655
+ target,
1656
+ sample(reading)
1657
+ ])),
1658
+ subscribe: (targets, listener) => monitor.watch(names(targets), (target, reading) => listener(target, sample(reading))),
1659
+ close: async () => {
1660
+ controller.abort();
1661
+ await running;
1662
+ }
1663
+ };
1664
+ };
1665
+ export {
1666
+ DebugError,
1667
+ LoginUnavailable,
1668
+ createDebugHandler,
1669
+ createDebugger,
1670
+ createLoginAdapter,
1671
+ createOpcUaAdapter,
1672
+ createPaths,
1673
+ loadManifest,
1674
+ noLogin,
1675
+ opcUaConnection,
1676
+ serveDebugger,
1677
+ timelineEntries
1678
+ };
1679
+
1680
+ //# debugId=19B08BFA048A0C5264756E2164756E21
1681
+ //# sourceMappingURL=index.js.map