@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/login.ts
CHANGED
|
@@ -1,117 +1,35 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Connection,
|
|
4
|
+
TargetAdapter,
|
|
5
|
+
TargetRead,
|
|
6
|
+
TargetSample,
|
|
7
|
+
} from "./adapter.ts";
|
|
8
|
+
import { errorMessage } from "./errors.ts";
|
|
9
|
+
import { parseLoginValue } from "./login-value.ts";
|
|
3
10
|
import type { DebugType } from "./manifest.ts";
|
|
4
11
|
|
|
5
12
|
/** Why the login can't read now; `state` is what the debugger shows meanwhile. */
|
|
6
13
|
export class LoginUnavailable extends Error {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
constructor(
|
|
15
|
+
message: string,
|
|
16
|
+
options?: ErrorOptions & { state?: "connecting" | "unavailable" },
|
|
17
|
+
) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.state = options?.state ?? "unavailable";
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
readonly state: "connecting" | "unavailable";
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
export const noLogin =
|
|
25
|
+
export const noLogin =
|
|
26
|
+
"Live values need an engineering login: run `tsplc login`.";
|
|
16
27
|
|
|
17
28
|
/** How long to wait between attempts while there is no login to read through. */
|
|
18
29
|
const retryMs = 5000;
|
|
19
30
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
...["BYTE", "WORD", "DWORD", "LWORD"],
|
|
23
|
-
]);
|
|
24
|
-
const timeUnits: Record<string, number> = {
|
|
25
|
-
d: 86_400_000,
|
|
26
|
-
h: 3_600_000,
|
|
27
|
-
m: 60_000,
|
|
28
|
-
s: 1000,
|
|
29
|
-
ms: 1,
|
|
30
|
-
us: 0.001,
|
|
31
|
-
ns: 0.000_001,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const typeName = (type: DebugType) => (type.kind === "scalar" ? type.iec : type.kind);
|
|
35
|
-
|
|
36
|
-
const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
37
|
-
|
|
38
|
-
/** `16#FF`, `2#1010`, `-42`, with IEC's digit separators; beyond doubles' exact range, a string. */
|
|
39
|
-
const integer = (text: string) => {
|
|
40
|
-
const radix = /^(2|8|16)#([0-9a-f_]+)$/i.exec(text);
|
|
41
|
-
const digits = radix ? radix[2]!.replaceAll("_", "") : text.replaceAll("_", "");
|
|
42
|
-
if (!radix && !/^[+-]?\d+$/.test(digits)) return undefined;
|
|
43
|
-
const value = BigInt(radix ? `0${{ 2: "b", 8: "o", 16: "x" }[radix[1]!]}${digits}` : digits);
|
|
44
|
-
return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** `T#1h2m3s4ms`, `TIME#-1.5s`, `LTIME#5us`, in milliseconds. */
|
|
48
|
-
const duration = (text: string) => {
|
|
49
|
-
const body = /^L?T(?:IME)?#(-?)(.+)$/i.exec(text);
|
|
50
|
-
if (!body) return undefined;
|
|
51
|
-
const parts = [...body[2]!.replaceAll("_", "").matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|d|h|m|s)/gi)];
|
|
52
|
-
if (parts.map(([part]) => part).join("") !== body[2]!.replaceAll("_", "")) return undefined;
|
|
53
|
-
const ms = parts.reduce(
|
|
54
|
-
(total, [, amount, unit]) => total + Number(amount) * timeUnits[unit!.toLowerCase()]!,
|
|
55
|
-
0,
|
|
56
|
-
);
|
|
57
|
-
return body[1] ? -ms : ms;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** `'it$'s'` or `"wide"`, with IEC's `$` escapes. */
|
|
61
|
-
const quoted = (text: string) => {
|
|
62
|
-
const body = /^(['"])(.*)\1$/s.exec(text);
|
|
63
|
-
if (!body) return undefined;
|
|
64
|
-
return body[2]!.replace(/\$([0-9a-f]{2}|[$'"lnprt])/gi, (_, code: string) => {
|
|
65
|
-
if (code.length === 2) return String.fromCharCode(parseInt(code, 16));
|
|
66
|
-
const named: Record<string, string> = { l: "\n", n: "\n", p: "\f", r: "\r", t: "\t" };
|
|
67
|
-
return named[code.toLowerCase()] ?? code;
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A value as `read_value` returns it, in the form the debugger decodes: BOOL as a boolean,
|
|
73
|
-
* numbers and TIME (in ms) as numbers, enums as their member's number (or its name when the
|
|
74
|
-
* manifest doesn't list it), STRING unquoted, and arrays read whole as arrays. Undefined when the
|
|
75
|
-
* text doesn't fit the type.
|
|
76
|
-
*/
|
|
77
|
-
export const parseLoginValue = (type: DebugType, text: string): unknown => {
|
|
78
|
-
const trimmed = text.trim();
|
|
79
|
-
// Typed literals such as `INT#5`; TIME's own `T#` prefix is kept.
|
|
80
|
-
const untyped = trimmed.replace(/^(?!L?T(?:IME)?#)[a-z_]+#(?=.)/i, "");
|
|
81
|
-
switch (type.kind) {
|
|
82
|
-
case "enum": {
|
|
83
|
-
const number = integer(untyped);
|
|
84
|
-
if (typeof number === "number") return number;
|
|
85
|
-
const named = /^(?:\w+\.)*(\w+)(?:\s*\((-?\d+)\))?$/.exec(untyped);
|
|
86
|
-
if (!named) return undefined;
|
|
87
|
-
return type.members[named[1]!] ?? (named[2] === undefined ? named[1] : Number(named[2]));
|
|
88
|
-
}
|
|
89
|
-
case "array": {
|
|
90
|
-
const items = /^\[?(.*?)\]?$/s.exec(untyped)![1]!.trim();
|
|
91
|
-
if (!items) return [];
|
|
92
|
-
// Only raw assembly bytes are read whole; their elements are scalars named by IEC type.
|
|
93
|
-
const element = { kind: "scalar", iec: type.element } as const;
|
|
94
|
-
const values = items.split(/\s*,\s*/).map((item) => parseLoginValue(element, item));
|
|
95
|
-
return values.length === type.length && values.every((value) => value !== undefined)
|
|
96
|
-
? values
|
|
97
|
-
: undefined;
|
|
98
|
-
}
|
|
99
|
-
case "record":
|
|
100
|
-
return undefined;
|
|
101
|
-
case "scalar":
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
const iec = type.iec.toUpperCase();
|
|
105
|
-
if (iec === "BOOL") return { TRUE: true, FALSE: false, 1: true, 0: false }[untyped.toUpperCase()];
|
|
106
|
-
if (integerTypes.has(iec)) return integer(untyped);
|
|
107
|
-
if (iec === "REAL" || iec === "LREAL") {
|
|
108
|
-
const number = Number(untyped);
|
|
109
|
-
return untyped !== "" && !Number.isNaN(number) ? number : undefined;
|
|
110
|
-
}
|
|
111
|
-
if (iec === "TIME" || iec === "LTIME") return duration(trimmed);
|
|
112
|
-
if (/^W?STRING\b/.test(iec)) return quoted(trimmed) ?? trimmed;
|
|
113
|
-
return trimmed;
|
|
114
|
-
};
|
|
31
|
+
const typeName = (type: DebugType) =>
|
|
32
|
+
type.kind === "scalar" ? type.iec : type.kind;
|
|
115
33
|
|
|
116
34
|
/**
|
|
117
35
|
* Reads through an engineering login that answers `read(expressions)` with CODESYS's own text
|
|
@@ -123,162 +41,224 @@ export const parseLoginValue = (type: DebugType, text: string): unknown => {
|
|
|
123
41
|
* expressions that fail are found, and those are reported bad until the next login.
|
|
124
42
|
*/
|
|
125
43
|
export const createLoginAdapter = (options: {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
44
|
+
read: (expressions: string[]) => Promise<Record<string, string>>;
|
|
45
|
+
/** A target that always reads, such as the build id. */
|
|
46
|
+
probe: string;
|
|
47
|
+
endpoint: string;
|
|
48
|
+
samplingMs?: number;
|
|
131
49
|
}): TargetAdapter => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
50
|
+
const samplingMs = options.samplingMs ?? 1000;
|
|
51
|
+
let connection: Connection = {
|
|
52
|
+
state: "connecting",
|
|
53
|
+
since: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
const connectionListeners = new Set<(connection: Connection) => void>();
|
|
56
|
+
const subscriptions = new Set<{
|
|
57
|
+
targets: readonly TargetRead[];
|
|
58
|
+
listener: (target: string, sample: TargetSample) => void;
|
|
59
|
+
intervalMs: number;
|
|
60
|
+
due: number;
|
|
61
|
+
}>();
|
|
62
|
+
/** Expressions the controller refused, with its reason, until the login is lost. */
|
|
63
|
+
const rejected = new Map<string, string>();
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
|
|
66
|
+
const setConnection = (state: Connection["state"], error?: string) => {
|
|
67
|
+
if (controller.signal.aborted) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (connection.state === state && connection.error === error) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (state !== "connected") {
|
|
74
|
+
rejected.clear();
|
|
75
|
+
}
|
|
76
|
+
connection = {
|
|
77
|
+
state,
|
|
78
|
+
since: new Date().toISOString(),
|
|
79
|
+
...(error && { error }),
|
|
80
|
+
};
|
|
81
|
+
for (const listener of connectionListeners) {
|
|
82
|
+
listener(connection);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const split = async (
|
|
87
|
+
expressions: string[],
|
|
88
|
+
): Promise<Record<string, string>> => {
|
|
89
|
+
try {
|
|
90
|
+
return await options.read(expressions);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof LoginUnavailable) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
if (expressions.length === 1) {
|
|
96
|
+
rejected.set(expressions[0]!, errorMessage(error));
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
const half = Math.ceil(expressions.length / 2);
|
|
100
|
+
return {
|
|
101
|
+
...(await split(expressions.slice(0, half))),
|
|
102
|
+
...(await split(expressions.slice(half))),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const readText = async (expressions: string[]) => {
|
|
108
|
+
try {
|
|
109
|
+
return await options.read(expressions);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof LoginUnavailable) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
// One expression the controller can't read fails the whole read; a lost login fails the probe too.
|
|
115
|
+
await options.read([options.probe]);
|
|
116
|
+
return split(expressions);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
120
|
+
/** The login's text for each expression, or why it can't read; keeps `connection` current. */
|
|
121
|
+
const readTexts = async (
|
|
122
|
+
expressions: string[],
|
|
123
|
+
): Promise<{ texts: Record<string, string> } | { error: string }> => {
|
|
124
|
+
const overdue = setTimeout(
|
|
125
|
+
() => setConnection("lost", "The engineering login stopped answering"),
|
|
126
|
+
3 * samplingMs,
|
|
127
|
+
);
|
|
128
|
+
try {
|
|
129
|
+
const texts = expressions.length ? await readText(expressions) : {};
|
|
130
|
+
setConnection("connected");
|
|
131
|
+
return { texts };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
setConnection(
|
|
134
|
+
error instanceof LoginUnavailable ? error.state : "lost",
|
|
135
|
+
errorMessage(error),
|
|
136
|
+
);
|
|
137
|
+
return { error: errorMessage(error) };
|
|
138
|
+
} finally {
|
|
139
|
+
clearTimeout(overdue);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
152
142
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
143
|
+
/** A target's value from the login's text, or bad with the reason it has none. */
|
|
144
|
+
const parsedSample = (
|
|
145
|
+
{ target, type }: TargetRead,
|
|
146
|
+
text: string | undefined,
|
|
147
|
+
receivedAt: string,
|
|
148
|
+
): TargetSample => {
|
|
149
|
+
const value = text === undefined ? undefined : parseLoginValue(type, text);
|
|
150
|
+
if (value !== undefined) {
|
|
151
|
+
return { value, quality: "good", status: "Good", receivedAt };
|
|
152
|
+
}
|
|
153
|
+
const reason =
|
|
154
|
+
text === undefined
|
|
155
|
+
? "Not returned by the login"
|
|
156
|
+
: `Not a ${typeName(type)}`;
|
|
157
|
+
return {
|
|
158
|
+
value: text ?? null,
|
|
159
|
+
quality: "bad",
|
|
160
|
+
status: rejected.get(target) ?? reason,
|
|
161
|
+
receivedAt,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
169
164
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
/** Never fails: while the login can't read, every target is unavailable. */
|
|
166
|
+
const read = async (targets: readonly TargetRead[]) => {
|
|
167
|
+
const unique = [
|
|
168
|
+
...new Map(targets.map((target) => [target.target, target])).values(),
|
|
169
|
+
];
|
|
170
|
+
const result = await readTexts(
|
|
171
|
+
unique
|
|
172
|
+
.map(({ target }) => target)
|
|
173
|
+
.filter((target) => !rejected.has(target)),
|
|
174
|
+
);
|
|
175
|
+
const receivedAt = new Date().toISOString();
|
|
176
|
+
if ("error" in result) {
|
|
177
|
+
return new Map<string, TargetSample>(
|
|
178
|
+
unique.map(({ target }) => [
|
|
179
|
+
target,
|
|
180
|
+
{
|
|
181
|
+
value: null,
|
|
182
|
+
quality: "unavailable",
|
|
183
|
+
status: result.error,
|
|
184
|
+
receivedAt,
|
|
185
|
+
},
|
|
186
|
+
]),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return new Map(
|
|
190
|
+
unique.map((read) => [
|
|
191
|
+
read.target,
|
|
192
|
+
parsedSample(read, result.texts[read.target], receivedAt),
|
|
193
|
+
]),
|
|
194
|
+
);
|
|
195
|
+
};
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let values: Record<string, string>;
|
|
189
|
-
const overdue = setTimeout(
|
|
190
|
-
() => setConnection("lost", "The engineering login stopped answering"),
|
|
191
|
-
3 * samplingMs,
|
|
192
|
-
);
|
|
193
|
-
try {
|
|
194
|
-
values = expressions.length ? await readText(expressions) : {};
|
|
195
|
-
setConnection("connected");
|
|
196
|
-
} catch (error) {
|
|
197
|
-
setConnection(error instanceof LoginUnavailable ? error.state : "lost", message(error));
|
|
198
|
-
const receivedAt = new Date().toISOString();
|
|
199
|
-
for (const { target } of unique)
|
|
200
|
-
samples.set(target, {
|
|
201
|
-
value: null,
|
|
202
|
-
quality: "unavailable",
|
|
203
|
-
status: message(error),
|
|
204
|
-
receivedAt,
|
|
205
|
-
});
|
|
206
|
-
return samples;
|
|
207
|
-
} finally {
|
|
208
|
-
clearTimeout(overdue);
|
|
209
|
-
}
|
|
210
|
-
const receivedAt = new Date().toISOString();
|
|
211
|
-
for (const { target, type } of unique) {
|
|
212
|
-
const text = values[target];
|
|
213
|
-
const refused = rejected.get(target);
|
|
214
|
-
const value = text === undefined ? undefined : parseLoginValue(type, text);
|
|
215
|
-
samples.set(
|
|
216
|
-
target,
|
|
217
|
-
value !== undefined
|
|
218
|
-
? { value, quality: "good", status: "Good", receivedAt }
|
|
219
|
-
: {
|
|
220
|
-
value: text ?? null,
|
|
221
|
-
quality: "bad",
|
|
222
|
-
status:
|
|
223
|
-
refused ??
|
|
224
|
-
(text === undefined ? "Not returned by the login" : `Not a ${typeName(type)}`),
|
|
225
|
-
receivedAt,
|
|
226
|
-
},
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
return samples;
|
|
230
|
-
};
|
|
197
|
+
/** Until the next poll: the rest of the interval, as long again after an overrun, longer without a login. */
|
|
198
|
+
const pauseAfter = (tookMs: number) => {
|
|
199
|
+
if (connection.state !== "connected" && connection.state !== "connecting") {
|
|
200
|
+
return retryMs;
|
|
201
|
+
}
|
|
202
|
+
return tookMs > samplingMs ? tookMs : samplingMs - tookMs;
|
|
203
|
+
};
|
|
231
204
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
205
|
+
const poll = async () => {
|
|
206
|
+
while (!controller.signal.aborted) {
|
|
207
|
+
const started = Date.now();
|
|
208
|
+
// Timers fire a little early or late; a subscription due within half an interval is read now.
|
|
209
|
+
const due = [...subscriptions].filter(
|
|
210
|
+
({ due }) => due <= started + samplingMs / 2,
|
|
211
|
+
);
|
|
212
|
+
if (due.length) {
|
|
213
|
+
for (const subscription of due) {
|
|
214
|
+
subscription.due = started + subscription.intervalMs;
|
|
215
|
+
}
|
|
216
|
+
// Closing the debugger must not wait for an engineering worker that stopped answering.
|
|
217
|
+
const stopped = Promise.withResolvers<void>();
|
|
218
|
+
const stop = () => stopped.resolve();
|
|
219
|
+
controller.signal.addEventListener("abort", stop, { once: true });
|
|
220
|
+
const samples = await Promise.race([
|
|
221
|
+
read(due.flatMap(({ targets }) => targets)),
|
|
222
|
+
stopped.promise,
|
|
223
|
+
]).finally(() => controller.signal.removeEventListener("abort", stop));
|
|
224
|
+
if (!samples || controller.signal.aborted) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
for (const subscription of due) {
|
|
228
|
+
if (subscriptions.has(subscription)) {
|
|
229
|
+
for (const { target } of subscription.targets) {
|
|
230
|
+
subscription.listener(target, samples.get(target)!);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await delay(pauseAfter(Date.now() - started), undefined, {
|
|
236
|
+
signal: controller.signal,
|
|
237
|
+
}).catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const running = poll();
|
|
261
241
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
242
|
+
return {
|
|
243
|
+
method: "engineering login",
|
|
244
|
+
endpoint: options.endpoint,
|
|
245
|
+
samplingMs,
|
|
246
|
+
connection: () => connection,
|
|
247
|
+
onConnection: (listener) => {
|
|
248
|
+
connectionListeners.add(listener);
|
|
249
|
+
return () => connectionListeners.delete(listener);
|
|
250
|
+
},
|
|
251
|
+
read,
|
|
252
|
+
subscribe: (targets, listener, intervalMs = samplingMs) => {
|
|
253
|
+
const subscription = { targets, listener, intervalMs, due: 0 };
|
|
254
|
+
subscriptions.add(subscription);
|
|
255
|
+
return () => subscriptions.delete(subscription);
|
|
256
|
+
},
|
|
257
|
+
close: async () => {
|
|
258
|
+
controller.abort();
|
|
259
|
+
subscriptions.clear();
|
|
260
|
+
connectionListeners.clear();
|
|
261
|
+
await running;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
284
264
|
};
|