@tsplc/debug 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +126 -123
- 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/manifest.ts
CHANGED
|
@@ -9,178 +9,180 @@ import { z } from "zod";
|
|
|
9
9
|
|
|
10
10
|
/** A location in the project's own sources: `file` indexes `sources`; lines and columns are 1-based. */
|
|
11
11
|
export const sourceRange = z.object({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
file: z.number().int(),
|
|
13
|
+
line: z.number().int(),
|
|
14
|
+
column: z.number().int(),
|
|
15
|
+
endLine: z.number().int(),
|
|
16
|
+
endColumn: z.number().int(),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const json: z.ZodType<unknown> = z.lazy(() =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
z.union([
|
|
21
|
+
z.string(),
|
|
22
|
+
z.number(),
|
|
23
|
+
z.boolean(),
|
|
24
|
+
z.null(),
|
|
25
|
+
z.array(json),
|
|
26
|
+
z.record(z.string(), json),
|
|
27
|
+
]),
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
export const debugField = z.object({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
name: z.string(),
|
|
32
|
+
target: z.string(),
|
|
33
|
+
type: z.string(),
|
|
34
|
+
declaration: sourceRange.optional(),
|
|
35
|
+
/** Compiler-made storage, such as an async job's `step`; hidden unless asked for. */
|
|
36
|
+
generated: z.literal(true).optional(),
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
/** Keys of `types` are opaque ids; scalars use their IEC name, such as `BOOL`. */
|
|
40
40
|
export const debugType = z.discriminatedUnion("kind", [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
41
|
+
z.object({ kind: z.literal("scalar"), iec: z.string() }),
|
|
42
|
+
z.object({
|
|
43
|
+
kind: z.literal("enum"),
|
|
44
|
+
name: z.string(),
|
|
45
|
+
members: z.record(z.string(), z.number().int()),
|
|
46
|
+
}),
|
|
47
|
+
z.object({
|
|
48
|
+
kind: z.literal("record"),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
declaration: sourceRange.optional(),
|
|
51
|
+
fields: z.array(debugField),
|
|
52
|
+
}),
|
|
53
|
+
z.object({
|
|
54
|
+
kind: z.literal("array"),
|
|
55
|
+
element: z.string(),
|
|
56
|
+
length: z.number().int(),
|
|
57
|
+
/** The first index; TypeScript paths use the target's own indexes, `lower` to `lower + length - 1`. */
|
|
58
|
+
lower: z.number().int(),
|
|
59
|
+
}),
|
|
60
60
|
]);
|
|
61
61
|
|
|
62
62
|
export const debugRoot = z.object({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
path: z.string(),
|
|
64
|
+
target: z.string(),
|
|
65
|
+
type: z.string(),
|
|
66
|
+
declaration: sourceRange.optional(),
|
|
67
|
+
/** Readable at runtime through the target's adapter; for CODESYS, all stored state is. */
|
|
68
|
+
readable: z.boolean(),
|
|
69
|
+
generated: z.literal(true).optional(),
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
/** A field the compiler folded away: it has no runtime storage, but its value is known. */
|
|
73
73
|
export const debugConstant = z.object({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
path: z.string(),
|
|
75
|
+
type: z.string(),
|
|
76
|
+
value: json,
|
|
77
|
+
declaration: sourceRange.optional(),
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
export const debugIo = z.object({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
path: z.string(),
|
|
82
|
+
target: z.string(),
|
|
83
|
+
direction: z.enum(["input", "output"]),
|
|
84
|
+
type: z.string(),
|
|
85
|
+
device: z.string().optional(),
|
|
86
|
+
channel: z.string().optional(),
|
|
87
|
+
address: z.string().optional(),
|
|
88
|
+
inverted: z.literal(true).optional(),
|
|
89
|
+
shared: z.literal(true).optional(),
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
export const debugChannel = z.object({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
id: z.string(),
|
|
94
|
+
name: z.string(),
|
|
95
|
+
direction: z.enum(["input", "output"]),
|
|
96
|
+
type: z.string(),
|
|
97
|
+
offsetBits: z.number().int(),
|
|
98
|
+
bits: z.number().int(),
|
|
99
|
+
address: z.string().optional(),
|
|
100
|
+
unit: z.string().optional(),
|
|
101
|
+
description: z.string().optional(),
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
export const debugDevice = z.object({
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
105
|
+
id: z.string(),
|
|
106
|
+
product: z.string().optional(),
|
|
107
|
+
vendor: z.string().optional(),
|
|
108
|
+
productCode: z.string().optional(),
|
|
109
|
+
network: z
|
|
110
|
+
.object({ address: z.string(), rpiMs: z.number().optional() })
|
|
111
|
+
.optional(),
|
|
112
|
+
connection: z.string().optional(),
|
|
113
|
+
assemblies: z.array(
|
|
114
|
+
z.object({
|
|
115
|
+
direction: z.enum(["input", "output"]),
|
|
116
|
+
bytes: z.number().int(),
|
|
117
|
+
address: z.string().optional(),
|
|
118
|
+
/** A byte array over the whole assembly, mapped or not. */
|
|
119
|
+
raw: z.string().optional(),
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
/**
|
|
123
|
+
* Target paths of the driver's own state, such as a generated adapter's `eState`; absent means
|
|
124
|
+
* the device's health is unknown.
|
|
125
|
+
*/
|
|
126
|
+
health: z
|
|
127
|
+
.object({
|
|
128
|
+
state: z.string().optional(),
|
|
129
|
+
states: z.record(z.string(), z.number().int()).optional(),
|
|
130
|
+
diagnostic: z.string().optional(),
|
|
131
|
+
message: z.string().optional(),
|
|
132
|
+
})
|
|
133
|
+
.optional(),
|
|
134
|
+
/** Status symbols by label: hand-configured ones, and a generated Modbus server's error. */
|
|
135
|
+
status: z.record(z.string(), z.string()).optional(),
|
|
136
|
+
channels: z.array(debugChannel),
|
|
135
137
|
});
|
|
136
138
|
|
|
137
139
|
/** A specialized function: one per method per instance, so `reads` resolve to concrete paths. */
|
|
138
140
|
export const debugFunction = z.object({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
id: z.number().int(),
|
|
142
|
+
name: z.string(),
|
|
143
|
+
declaration: sourceRange,
|
|
144
|
+
/** The TypeScript path of `this`, for a method specialized to one instance. */
|
|
145
|
+
instance: z.string().optional(),
|
|
146
|
+
task: z.string().optional(),
|
|
147
|
+
target: z.string(),
|
|
148
|
+
/** Member reads in the body whose runtime path is static in this specialization. */
|
|
149
|
+
reads: z.array(z.object({ range: sourceRange, path: z.string() })),
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
export const debugJob = z.object({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
153
|
+
id: z.number().int(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
declaration: sourceRange,
|
|
156
|
+
instance: z.string().optional(),
|
|
157
|
+
/** The job's state, with fields `step`, `busy`, `done`, `error` and `cancelling`. */
|
|
158
|
+
target: z.string(),
|
|
159
|
+
steps: z.array(
|
|
160
|
+
z.object({
|
|
161
|
+
step: z.number().int(),
|
|
162
|
+
kind: z.enum(["await", "return", "throw", "statement"]),
|
|
163
|
+
range: sourceRange,
|
|
164
|
+
text: z.string(),
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
/**
|
|
168
|
+
* The PLC clock in milliseconds when the job last started: the clock of telemetry records'
|
|
169
|
+
* `time` and a batch's `now`.
|
|
170
|
+
*/
|
|
171
|
+
startedAt: z.string().optional(),
|
|
172
|
+
/**
|
|
173
|
+
* Service calls the job awaits, its nested jobs' included; `sequence` is the request counter sent
|
|
174
|
+
* as `x-tsplc-request`, with `job`'s id when a nested job makes the call.
|
|
175
|
+
*/
|
|
176
|
+
requests: z.array(
|
|
177
|
+
z.object({
|
|
178
|
+
service: z.string(),
|
|
179
|
+
target: z.string(),
|
|
180
|
+
sequence: z.string(),
|
|
181
|
+
job: z.number().int().optional(),
|
|
182
|
+
}),
|
|
183
|
+
),
|
|
184
|
+
/** Made by the compiler, such as a service call's own job; hidden unless asked for. */
|
|
185
|
+
generated: z.literal(true).optional(),
|
|
184
186
|
});
|
|
185
187
|
|
|
186
188
|
/**
|
|
@@ -188,71 +190,75 @@ export const debugJob = z.object({
|
|
|
188
190
|
* different value, or a job starting or finishing. Trace records name their site by `id`.
|
|
189
191
|
*/
|
|
190
192
|
export const debugEventSite = z.object({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
193
|
+
id: z.number().int(),
|
|
194
|
+
kind: z.enum(["state", "decision", "job"]),
|
|
195
|
+
/** For `kind: "job"`: the job, and whether it starts or finishes at this site. */
|
|
196
|
+
job: z.number().int().optional(),
|
|
197
|
+
phase: z.enum(["started", "finished"]).optional(),
|
|
198
|
+
/** The TypeScript path written, or of the job. */
|
|
199
|
+
path: z.string(),
|
|
200
|
+
function: z.number().int().optional(),
|
|
201
|
+
type: z.string(),
|
|
202
|
+
range: sourceRange,
|
|
203
|
+
text: z.string(),
|
|
204
|
+
task: z.string(),
|
|
205
|
+
/**
|
|
206
|
+
* Guard values captured in the same scan, at the write: each `if` condition leaf enclosing it.
|
|
207
|
+
* `stable` is false when the body between the guard and the write can change the leaf.
|
|
208
|
+
*/
|
|
209
|
+
operands: z.array(
|
|
210
|
+
z.object({
|
|
211
|
+
text: z.string(),
|
|
212
|
+
range: sourceRange,
|
|
213
|
+
type: z.string(),
|
|
214
|
+
path: z.string().optional(),
|
|
215
|
+
stable: z.boolean(),
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
216
218
|
});
|
|
217
219
|
|
|
218
220
|
/** The statement that last wrote each output, per scan. */
|
|
219
221
|
export const debugWriter = z.object({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
output: z.string(),
|
|
223
|
+
target: z.string(),
|
|
224
|
+
sites: z.array(
|
|
225
|
+
z.object({
|
|
226
|
+
id: z.number().int(),
|
|
227
|
+
range: sourceRange,
|
|
228
|
+
function: z.number().int(),
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
225
231
|
});
|
|
226
232
|
|
|
227
233
|
export const debugTask = z.object({
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
name: z.string(),
|
|
235
|
+
intervalMs: z.number(),
|
|
236
|
+
priority: z.number().int(),
|
|
231
237
|
});
|
|
232
238
|
|
|
233
239
|
export const debugManifest = z.object({
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
240
|
+
kind: z.literal("tsplc-debug"),
|
|
241
|
+
version: z.literal(1),
|
|
242
|
+
build: z.object({
|
|
243
|
+
id: z.string(),
|
|
244
|
+
/** A variable holding `id`, to tell which build the controller runs. */
|
|
245
|
+
target: z.string(),
|
|
246
|
+
compiler: z.string(),
|
|
247
|
+
}),
|
|
248
|
+
/** Project-relative paths. */
|
|
249
|
+
sources: z.array(z.string()),
|
|
250
|
+
types: z.record(z.string(), debugType),
|
|
251
|
+
roots: z.array(debugRoot),
|
|
252
|
+
constants: z.array(debugConstant),
|
|
253
|
+
io: z.array(debugIo),
|
|
254
|
+
devices: z.array(debugDevice),
|
|
255
|
+
functions: z.array(debugFunction),
|
|
256
|
+
jobs: z.array(debugJob),
|
|
257
|
+
events: z.array(debugEventSite),
|
|
258
|
+
writers: z.array(debugWriter),
|
|
259
|
+
tasks: z.array(debugTask),
|
|
260
|
+
/** Job error codes by number: 1 cancelled, 2 timeout… */
|
|
261
|
+
errors: z.record(z.string(), z.string()),
|
|
256
262
|
});
|
|
257
263
|
|
|
258
264
|
export type SourceRange = z.infer<typeof sourceRange>;
|
package/src/opcua.ts
CHANGED
|
@@ -1,33 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createMonitor,
|
|
3
|
+
monitorConnection,
|
|
4
|
+
type MonitorOptions,
|
|
5
|
+
type Reading,
|
|
6
|
+
} from "@tsplc/opcua";
|
|
3
7
|
import type { ResolvedTsplcConfig } from "@tsplc/config";
|
|
4
8
|
import type { TargetAdapter, TargetRead, TargetSample } from "./adapter.ts";
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The connection the config's `monitor` section describes. Its certificate directory is relative
|
|
10
|
-
* to `root`, and the password comes from `passwordEnv` in `env`.
|
|
11
|
-
*/
|
|
10
|
+
/** The config's `monitor` connection, with the user's password from `passwordEnv` in `env`. */
|
|
12
11
|
export const opcUaConnection = (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
monitor: NonNullable<ResolvedTsplcConfig["monitor"]>,
|
|
13
|
+
options: { root: string; env: Record<string, string | undefined> },
|
|
15
14
|
): MonitorOptions => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
15
|
+
const password = monitor.passwordEnv
|
|
16
|
+
? options.env[monitor.passwordEnv]
|
|
17
|
+
: undefined;
|
|
18
|
+
if (monitor.username && !password) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
monitor.passwordEnv
|
|
21
|
+
? `Set ${monitor.passwordEnv} to the OPC UA password of ${monitor.username}.`
|
|
22
|
+
: "Configure monitor.passwordEnv for the OPC UA user.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return monitorConnection(monitor, {
|
|
26
|
+
root: options.root,
|
|
27
|
+
password: password ?? null,
|
|
28
|
+
});
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
/**
|
|
@@ -35,35 +33,37 @@ export const opcUaConnection = (
|
|
|
35
33
|
* what the build publishes, so it suits a local virtual PLC; samples arrive on change.
|
|
36
34
|
*/
|
|
37
35
|
export const createOpcUaAdapter = (options: MonitorOptions): TargetAdapter => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
const monitor = createMonitor(options);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const running = monitor.run(controller.signal);
|
|
39
|
+
const sample = ({ value, quality, status }: Reading): TargetSample => ({
|
|
40
|
+
value,
|
|
41
|
+
quality,
|
|
42
|
+
status,
|
|
43
|
+
receivedAt: new Date().toISOString(),
|
|
44
|
+
});
|
|
45
|
+
const names = (targets: readonly TargetRead[]) =>
|
|
46
|
+
targets.map(({ target }) => target);
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
return {
|
|
49
|
+
method: "OPC UA",
|
|
50
|
+
endpoint: options.endpoint,
|
|
51
|
+
samplingMs: monitor.samplingMs,
|
|
52
|
+
connection: monitor.status,
|
|
53
|
+
onConnection: monitor.onStatus,
|
|
54
|
+
read: async (targets) =>
|
|
55
|
+
new Map(
|
|
56
|
+
Object.entries(await monitor.read(names(targets))).map(
|
|
57
|
+
([target, reading]) => [target, sample(reading)],
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
subscribe: (targets, listener) =>
|
|
61
|
+
monitor.watch(names(targets), (target, reading) =>
|
|
62
|
+
listener(target, sample(reading)),
|
|
63
|
+
),
|
|
64
|
+
close: async () => {
|
|
65
|
+
controller.abort();
|
|
66
|
+
await running;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
69
|
};
|