@2702rebels/ntcore 1.0.0

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.mjs ADDED
@@ -0,0 +1,816 @@
1
+ import { Decoder, Encoder } from "@msgpack/msgpack";
2
+ import { Logger } from "@2702rebels/logger";
3
+
4
+ //#region src/NT4Spec.ts
5
+ /**
6
+ * Network constants.
7
+ */
8
+ const NetworkSpecs = {
9
+ portUnsecure: 5810,
10
+ portSecure: 5811,
11
+ protocol41: "v4.1.networktables.first.wpi.edu",
12
+ protocol40: "networktables.first.wpi.edu",
13
+ protocolRTT: "rtt.networktables.first.wpi.edu",
14
+ pingPeriod40: 1e3,
15
+ pingTimeout40: 3e3,
16
+ pingPeriod41: 250,
17
+ pingTimeout41: 1e3
18
+ };
19
+ /**
20
+ * Supported data types mapped to protocol identifiers.
21
+ *
22
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#supported-data-types
23
+ */
24
+ const DataTypeCodes = {
25
+ boolean: 0,
26
+ double: 1,
27
+ int: 2,
28
+ float: 3,
29
+ string: 4,
30
+ json: 4,
31
+ raw: 5,
32
+ rpc: 5,
33
+ msgpack: 5,
34
+ protobuf: 5,
35
+ "boolean[]": 16,
36
+ "double[]": 17,
37
+ "int[]": 18,
38
+ "float[]": 19,
39
+ "string[]": 20
40
+ };
41
+
42
+ //#endregion
43
+ //#region src/NT4Subscription.ts
44
+ /** Contains information about NT4 subscription. */
45
+ var NT4Subscription = class {
46
+ /** Subscription UID. */
47
+ uid;
48
+ /** Subscription options. */
49
+ options;
50
+ /** Subscribed topic names or prefixes. */
51
+ topics;
52
+ constructor(uid, topics, options) {
53
+ this.uid = uid;
54
+ this.topics = new Set(typeof topics === "string" ? [topics] : topics);
55
+ this.options = options ?? {};
56
+ }
57
+ };
58
+
59
+ //#endregion
60
+ //#region src/NT4Topic.ts
61
+ /** Contains information about NT4 topic. */
62
+ var NT4Topic = class {
63
+ /** Topic UID. */
64
+ uid;
65
+ /** Topic name. */
66
+ name;
67
+ /** Topic data type. */
68
+ type;
69
+ /** Topic properties. */
70
+ properties;
71
+ /** Publisher UID. */
72
+ pubuid;
73
+ /** Last retained value. */
74
+ retainedValue;
75
+ /** Timestamp of {@link retainedValue}. */
76
+ retainedTimestamp;
77
+ constructor(uid, name, type, properties, pubuid) {
78
+ this.uid = uid;
79
+ this.name = name;
80
+ this.type = type;
81
+ this.properties = properties ?? {};
82
+ this.pubuid = pubuid;
83
+ this.typeCode = DataTypeCodes[type] ?? 5;
84
+ }
85
+ /** Merges properties with this topic. */
86
+ mergeProperties(properties) {
87
+ Object.entries(properties).forEach(([key, value]) => {
88
+ if (value === null) delete this.properties[key];
89
+ else this.properties[key] = value;
90
+ });
91
+ }
92
+ /** Updates retained value if the incoming timestamp is newer (greater-than or equal-to). */
93
+ updateRetainedValue(value, timestamp) {
94
+ if (this.retainedTimestamp == null || timestamp >= this.retainedTimestamp) {
95
+ this.retainedValue = value;
96
+ this.retainedTimestamp = timestamp;
97
+ }
98
+ }
99
+ /** Removes retained value except for "weak" default value. */
100
+ removeRetainedValue() {
101
+ if (this.retainedTimestamp != 0) {
102
+ this.retainedValue = void 0;
103
+ this.retainedTimestamp = void 0;
104
+ }
105
+ }
106
+ /** Resets retained value timestamp except for "weak" default value. */
107
+ resetRetainedTimestamp() {
108
+ if (this.retainedTimestamp != null && this.retainedTimestamp > 1) this.retainedTimestamp = 1;
109
+ }
110
+ /** Topic type code. For internal use only. */
111
+ typeCode;
112
+ };
113
+
114
+ //#endregion
115
+ //#region src/NT4WebSocketAsync.ts
116
+ const PROTOCOL_DATA = [NetworkSpecs.protocol41, NetworkSpecs.protocol40];
117
+ const PROTOCOL_RTT = [NetworkSpecs.protocolRTT];
118
+ /**
119
+ * A wrapper for `WebSocket` that implements async `connect` and `close` methods.
120
+ *
121
+ * This class manages the lifecycle of the raw websocket internally, creating and
122
+ * destroying the websocket as necessary when establishing and closing the connection.
123
+ */
124
+ var NT4WebSocketAsync = class {
125
+ url;
126
+ rtt;
127
+ socket = null;
128
+ onMessage;
129
+ onClose;
130
+ /**
131
+ * Creates a `WebSocket` wrapper to be used for NT4 communication channel.
132
+ * This constructor does not initiate the connection, see `connect`.
133
+ *
134
+ * @param address server network address
135
+ * @param port sever port
136
+ * @param clientId client identifier
137
+ * @param options connection options
138
+ */
139
+ constructor(address, port, clientId, options) {
140
+ this.url = `${options.secure ? "wss" : "ws"}://${address}:${port}/nt/${clientId}`;
141
+ this.rtt = options.rtt ?? false;
142
+ this.onMessage = options.onMessage;
143
+ this.onClose = options.onClose;
144
+ }
145
+ onMessageLocal(ev, source) {
146
+ if (this.onMessage && this.socket === source) this.onMessage(ev);
147
+ }
148
+ onCloseLocal(ev, source) {
149
+ if (this.onClose && this.socket === source) this.onClose(ev);
150
+ }
151
+ /** Returns `true` if the socket is in the connected state. */
152
+ get connected() {
153
+ return this.socket != null && this.socket.readyState === WebSocket.OPEN;
154
+ }
155
+ /** Returns `true` if the socket is in the connecting state. */
156
+ get connecting() {
157
+ return this.socket != null && this.socket.readyState === WebSocket.CONNECTING;
158
+ }
159
+ /** Returns the subprotocol negotiated with the server. */
160
+ get protocol() {
161
+ return this.socket?.protocol;
162
+ }
163
+ /** Sends text data frame (JSON serialized). */
164
+ sendText(data) {
165
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(Array.isArray(data) ? data : [data]));
166
+ }
167
+ /** Sends binary data frame. */
168
+ sendBinary(data) {
169
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) this.socket.send(data);
170
+ }
171
+ /**
172
+ * Asynchronously terminates the connection.
173
+ *
174
+ * @param code optional code
175
+ * @param reason optional reason
176
+ */
177
+ async close(code, reason) {
178
+ if (this.socket) {
179
+ const socket = this.socket;
180
+ this.socket = null;
181
+ return new Promise((resolve) => {
182
+ socket.addEventListener("close", () => {
183
+ resolve();
184
+ });
185
+ socket.close(code, reason);
186
+ });
187
+ }
188
+ return Promise.resolve();
189
+ }
190
+ /** Asynchronously initiates the connection. */
191
+ async connect() {
192
+ return new Promise((resolve, reject) => {
193
+ const socket = new WebSocket(this.url, this.rtt ? PROTOCOL_RTT : PROTOCOL_DATA);
194
+ socket.binaryType = "arraybuffer";
195
+ this.socket = socket;
196
+ socket.addEventListener("message", (_) => this.onMessageLocal(_, socket));
197
+ socket.addEventListener("close", (_) => this.onCloseLocal(_, socket));
198
+ socket.onopen = () => {
199
+ resolve();
200
+ };
201
+ socket.onerror = (event) => {
202
+ reject(event);
203
+ };
204
+ });
205
+ }
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/NT4Client.ts
210
+ /** Determines whether the specified predicate callback returns true for any element of a map. */
211
+ function some(map, predicate) {
212
+ for (const v of map.values()) if (predicate(v)) return true;
213
+ return false;
214
+ }
215
+ /**
216
+ * Implements a NetworkTable4 client that is responsible for establishing and maintaining a connection.
217
+ */
218
+ var NT4Client = class NT4Client {
219
+ port;
220
+ clientId;
221
+ dataChannel;
222
+ rttChannel;
223
+ decoder = new Decoder();
224
+ encoder = new Encoder();
225
+ onConnect;
226
+ onDisconnect;
227
+ onTopicAnnounced;
228
+ onTopicRemoved;
229
+ onTopicUpdated;
230
+ onDataReceived;
231
+ retryPolicy;
232
+ pingTimeoutOverride_ms;
233
+ subscriptions = /* @__PURE__ */ new Map();
234
+ publishedTopics = /* @__PURE__ */ new Map();
235
+ announcedTopics = /* @__PURE__ */ new Map();
236
+ announcedTopicsIndex = /* @__PURE__ */ new Map();
237
+ connectionEstablished = false;
238
+ connectionInitiated = false;
239
+ connectionAttempts = 0;
240
+ connectionState = "disconnected";
241
+ networkLatency_μs = null;
242
+ serverTimeOffset_μs = null;
243
+ pingTimeout_ms = 0;
244
+ pongTimestamp_μs = 0;
245
+ pingIntervalId = null;
246
+ pongIntervalId = null;
247
+ /** Truncated at 15 seconds exponential backoff retry policy starting at 1 second. */
248
+ static defaultRetryPolicy(attempts) {
249
+ return Math.round(Math.min(15, Math.max(1, 1.2 ** Math.min(15, attempts))) * 1e3);
250
+ }
251
+ /** Cleans up closed data channel. */
252
+ cleanup() {
253
+ Logger.Default.debug(`[NT4Client] cleaning up`);
254
+ if (this.pingIntervalId) {
255
+ clearInterval(this.pingIntervalId);
256
+ this.pingIntervalId = null;
257
+ }
258
+ if (this.pongIntervalId) {
259
+ clearInterval(this.pongIntervalId);
260
+ this.pongIntervalId = null;
261
+ }
262
+ this.rttChannel.close();
263
+ if (this.connectionEstablished && this.onDisconnect) this.onDisconnect();
264
+ this.connectionEstablished = false;
265
+ this.connectionState = "disconnected";
266
+ this.announcedTopics.clear();
267
+ this.announcedTopicsIndex.clear();
268
+ for (const topic of this.publishedTopics.values()) topic.resetRetainedTimestamp();
269
+ this.pongTimestamp_μs = 0;
270
+ this.serverTimeOffset_μs = null;
271
+ this.networkLatency_μs = null;
272
+ }
273
+ /** Attempts to establish a connection, continuously retrying using policy. */
274
+ async connectWithRetry() {
275
+ this.connectionState = "connecting";
276
+ if (!this.connectionInitiated) {
277
+ Logger.Default.debug(`[NT4Client] pending cancellation, aborting...`);
278
+ return;
279
+ }
280
+ let response = null;
281
+ try {
282
+ response = await fetch(`http://${this.serverAddress}:${this.port}`, { signal: AbortSignal.timeout(250) });
283
+ } catch {}
284
+ if (!this.connectionInitiated) {
285
+ Logger.Default.debug(`[NT4Client] pending cancellation, aborting...`);
286
+ return;
287
+ }
288
+ if (response && response.ok) try {
289
+ Logger.Default.debug(`[NT4Client] opening channel to ${this.serverAddress}:${this.port}`);
290
+ await this.dataChannel.connect();
291
+ this.connectionAttempts = 0;
292
+ if (!this.connectionInitiated) {
293
+ Logger.Default.debug(`[NT4Client] pending cancellation, aborting...`);
294
+ return;
295
+ }
296
+ this.connectionEstablished = true;
297
+ this.connectionState = "connected";
298
+ Logger.Default.debug(`[NT4Client] connected to ${this.serverAddress}:${this.port}, subprotocol = ${this.dataChannel.protocol}`);
299
+ let pingPeriod_ms = NetworkSpecs.pingPeriod40;
300
+ this.pingTimeout_ms = this.pingTimeoutOverride_ms ?? NetworkSpecs.pingTimeout40;
301
+ if (this.dataChannel.protocol === NetworkSpecs.protocol41) {
302
+ Logger.Default.debug(`[NT4Client] server speaks v4.1, creating separate RTT channel`);
303
+ await this.rttChannel.connect();
304
+ if (!this.connectionInitiated) {
305
+ Logger.Default.debug(`[NT4Client] pending cancellation, aborting...`);
306
+ return;
307
+ }
308
+ pingPeriod_ms = NetworkSpecs.pingPeriod41;
309
+ this.pingTimeout_ms = this.pingTimeoutOverride_ms ?? NetworkSpecs.pingTimeout41;
310
+ }
311
+ Logger.Default.debug(`[NT4Client] sending timestamp and re-establishing subscriptions`);
312
+ this.sendRTT();
313
+ this.pongTimestamp_μs = 0;
314
+ this.pingIntervalId = setInterval(() => this.sendRTT(), pingPeriod_ms);
315
+ this.pongIntervalId = setInterval(() => this.checkTimeout(), pingPeriod_ms);
316
+ for (const subscription of this.subscriptions.values()) this.subscribeCore(subscription);
317
+ for (const topic of this.publishedTopics.values()) this.publishCore(topic);
318
+ if (this.onConnect) this.onConnect();
319
+ return;
320
+ } catch (exception) {
321
+ Logger.Default.error(`[NT4Client] failed to open channel`, exception);
322
+ await this.rttChannel.close();
323
+ await this.dataChannel.close();
324
+ }
325
+ this.connectionAttempts++;
326
+ const timeout = this.retryPolicy(this.connectionAttempts);
327
+ if (timeout < 0) {
328
+ Logger.Default.debug(`[NT4Client] aborting per retry policy after ${this.connectionAttempts} attempts`);
329
+ this.disconnect();
330
+ return;
331
+ }
332
+ Logger.Default.debug(`[NT4Client] retrying connection in ${timeout.toFixed(0)}ms`);
333
+ setTimeout(() => this.connectWithRetry(), timeout);
334
+ }
335
+ /** Invoked once the client-server clock is synchronized. */
336
+ onClockSynchronized() {
337
+ Logger.Default.debug(`[NT4Client] clock synchronized, offset = ${this.serverTimeOffset_μs}`);
338
+ for (const topic of this.publishedTopics.values()) if (topic.retainedTimestamp != null && topic.retainedValue != null) this.sendValueCore(topic, topic.retainedValue, topic.retainedTimestamp === 1 ? this.serverTimeMicroseconds() ?? 1 : topic.retainedTimestamp);
339
+ }
340
+ /** Consumes received data frame. */
341
+ processDataFrame(data) {
342
+ if (typeof data === "string") {
343
+ let array;
344
+ try {
345
+ array = JSON.parse(data);
346
+ if (!Array.isArray(array)) {
347
+ Logger.Default.error(`[NT4Client] unexpected text frame, JSON must be an array, frame ignored`);
348
+ return;
349
+ }
350
+ } catch (exception) {
351
+ Logger.Default.error(`[NT4Client] unexpected text frame, JSON is malformed, frame ignored`, exception);
352
+ return;
353
+ }
354
+ array.forEach((d) => {
355
+ if (typeof d !== "object" || !("method" in d) || !("params" in d)) {
356
+ Logger.Default.error(`[NT4Client] malformed message in text frame, message ignored`);
357
+ return;
358
+ }
359
+ const { method, params } = d;
360
+ if (typeof method !== "string" || typeof params !== "object") {
361
+ Logger.Default.error(`[NT4Client] malformed message in text frame, message ignored`);
362
+ return;
363
+ }
364
+ switch (method) {
365
+ case "announce": {
366
+ const topic = new NT4Topic(params.id, params.name, params.type, params.properties, params.pubuid);
367
+ this.announcedTopics.set(topic.name, topic);
368
+ this.announcedTopicsIndex.set(topic.uid, topic);
369
+ if (this.onTopicAnnounced) this.onTopicAnnounced(topic);
370
+ return;
371
+ }
372
+ case "unannounce": {
373
+ const topic = this.announcedTopics.get(params.name);
374
+ if (topic) {
375
+ this.publishedTopics.get(topic.name)?.removeRetainedValue();
376
+ this.announcedTopics.delete(topic.name);
377
+ this.announcedTopicsIndex.delete(topic.uid);
378
+ if (this.onTopicRemoved) this.onTopicRemoved(topic);
379
+ }
380
+ return;
381
+ }
382
+ case "properties": {
383
+ const topic = this.announcedTopics.get(params.name);
384
+ if (topic) {
385
+ topic.mergeProperties(params.update);
386
+ if (this.onTopicUpdated) this.onTopicUpdated(topic);
387
+ }
388
+ return;
389
+ }
390
+ default:
391
+ Logger.Default.error(`[NT4Client] unexpected method '${method}' in text frame message, message ignored`);
392
+ return;
393
+ }
394
+ });
395
+ } else for (const d of this.decoder.decodeMulti(data)) {
396
+ const [topicId, timestamp, typeCode, value] = d;
397
+ if (typeof topicId !== "number") {
398
+ Logger.Default.error(`[NT4Client] unexpected non-numeric topic identifier, frame ignored`);
399
+ return;
400
+ }
401
+ if (typeof timestamp !== "number") {
402
+ Logger.Default.error(`[NT4Client] unexpected non-numeric timestamp, frame ignored`);
403
+ return;
404
+ }
405
+ if (typeof typeCode !== "number") {
406
+ Logger.Default.error(`[NT4Client] unexpected non-numeric typeCode, frame ignored`);
407
+ return;
408
+ }
409
+ if (topicId === -1) {
410
+ if (typeof value !== "number") {
411
+ Logger.Default.error(`[NT4Client] unexpected non-numeric value in RTT frame, message ignored`);
412
+ continue;
413
+ }
414
+ const clientTime_μs = this.getClientTimestamp_μs();
415
+ const latency_μs = (clientTime_μs - value) / 2;
416
+ if (this.networkLatency_μs == null || latency_μs < this.networkLatency_μs) {
417
+ const clockSynchronized = this.serverTimeOffset_μs == null;
418
+ this.serverTimeOffset_μs = Math.round(timestamp + latency_μs - clientTime_μs);
419
+ if (clockSynchronized) this.onClockSynchronized();
420
+ }
421
+ this.networkLatency_μs = latency_μs;
422
+ this.pongTimestamp_μs = clientTime_μs;
423
+ } else if (topicId > 0) {
424
+ const topic = this.announcedTopicsIndex.get(topicId);
425
+ if (topic == null) {
426
+ Logger.Default.error(`[NT4Client] unexpected topic identifier ${topicId}, message ignored`);
427
+ continue;
428
+ } else if (this.onDataReceived) {
429
+ this.publishedTopics.get(topic.name)?.updateRetainedValue(value, timestamp);
430
+ this.onDataReceived(topic, value, timestamp);
431
+ }
432
+ } else {
433
+ Logger.Default.error(`[NT4Client] invalid topic identifier ${topicId}, message ignored`);
434
+ continue;
435
+ }
436
+ }
437
+ }
438
+ /** Handles general data frame received. */
439
+ onChannelMessage = (ev) => {
440
+ this.processDataFrame(ev.data);
441
+ };
442
+ /** Handles RTT data frame received. */
443
+ onChannelMessageRTT = (ev) => {
444
+ if (typeof ev.data === "string") {
445
+ Logger.Default.debug(`[NT4Client] unexpected text data frame on RTT channel, frame ignored`);
446
+ return;
447
+ }
448
+ this.processDataFrame(ev.data);
449
+ };
450
+ /** Handles channel closing for whatever reason. */
451
+ onChannelClose = (ev) => {
452
+ Logger.Default.debug(`[NT4Client] channel closed`, ev);
453
+ this.cleanup();
454
+ this.connectWithRetry();
455
+ };
456
+ /** Returns current timestamp in microseconds. This is NOT wall clock time. */
457
+ getClientTimestamp_μs() {
458
+ return Math.round(performance.now() * 1e3);
459
+ }
460
+ /** Sends data frame to publish the topic. */
461
+ publishCore(topic) {
462
+ this.dataChannel.sendText({
463
+ method: "publish",
464
+ params: {
465
+ pubuid: topic.uid,
466
+ name: topic.name,
467
+ type: topic.type,
468
+ properties: topic.properties
469
+ }
470
+ });
471
+ }
472
+ /** Sends data frame to unpublish the topic. */
473
+ unpublishCore(topic) {
474
+ this.dataChannel.sendText({
475
+ method: "unpublish",
476
+ params: { pubuid: topic.uid }
477
+ });
478
+ }
479
+ /** Sends data frame to set topic properties. */
480
+ setPropertiesCore(name, properties) {
481
+ this.dataChannel.sendText({
482
+ method: "setproperties",
483
+ params: {
484
+ name,
485
+ update: properties
486
+ }
487
+ });
488
+ }
489
+ /** Sends data frame to subscribe the subscription. */
490
+ subscribeCore(subscription) {
491
+ this.dataChannel.sendText({
492
+ method: "subscribe",
493
+ params: {
494
+ subuid: subscription.uid,
495
+ topics: Array.from(subscription.topics),
496
+ options: subscription.options
497
+ }
498
+ });
499
+ }
500
+ /** Sends data frame to unsubscribe the subscription. */
501
+ unsubscribeCore(subscription) {
502
+ this.dataChannel.sendText({
503
+ method: "unsubscribe",
504
+ params: { subuid: subscription.uid }
505
+ });
506
+ }
507
+ /** Sends data frame to update topic value. */
508
+ sendValueCore(topic, value, timestamp) {
509
+ const frame = this.encoder.encode([
510
+ topic.uid,
511
+ timestamp,
512
+ topic.typeCode,
513
+ value
514
+ ]);
515
+ this.dataChannel.sendBinary(frame);
516
+ }
517
+ /**
518
+ * Sends RTT measurement binary frame.
519
+ * Uses RTT channel if available, falls back to data channel in NT4.0.
520
+ */
521
+ sendRTT() {
522
+ const timestamp = this.getClientTimestamp_μs();
523
+ const frame = this.encoder.encode([
524
+ -1,
525
+ 0,
526
+ DataTypeCodes.double,
527
+ timestamp
528
+ ]);
529
+ (this.rttChannel ?? this.dataChannel).sendBinary(frame);
530
+ }
531
+ /**
532
+ * Determines whether connection times out if no message has been received
533
+ * within the timeout interval since the last pong.
534
+ */
535
+ async checkTimeout() {
536
+ if (!this.connectionEstablished || this.pongTimestamp_μs === 0) return;
537
+ const delta_μs = this.getClientTimestamp_μs() - this.pongTimestamp_μs;
538
+ if (delta_μs > this.pingTimeout_ms * 1e3) {
539
+ Logger.Default.debug(`[NT4Client] no data received in the last ${(delta_μs / 1e3).toFixed(0)}ms, connection timed out`);
540
+ await this.dataChannel.close(4001, "timeout");
541
+ this.cleanup();
542
+ this.connectWithRetry();
543
+ }
544
+ }
545
+ /** Generates random UID. */
546
+ generateUID() {
547
+ return Math.floor(Math.random() * 2147483647);
548
+ }
549
+ /** Generates random safe UID avoiding possible clashes. */
550
+ generateSafeUID(has) {
551
+ let uid;
552
+ for (uid = this.generateUID(); has(uid); uid = this.generateUID());
553
+ return uid;
554
+ }
555
+ /** Determines whether published topic with the specified identifier exists. */
556
+ hasPublishedTopicWithId = (id) => some(this.publishedTopics, (_) => _.uid === id);
557
+ /** Determines whether subscription with the specified identifier exists. */
558
+ hasSubscriptionWithId = (id) => this.subscriptions.has(id);
559
+ /**
560
+ * Constructs an instance of NT4Client without initiating a connection.
561
+ *
562
+ * @param serverAddress NT4 server network address
563
+ * @param clientId this client identifier (cannot contain '@')
564
+ * @param options options
565
+ */
566
+ constructor(serverAddress, clientId, options) {
567
+ if (clientId.includes("@")) throw new Error("Client identifier must not contain '@' character");
568
+ this.serverAddress = serverAddress;
569
+ this.port = options.secure ? NetworkSpecs.portSecure : NetworkSpecs.portUnsecure;
570
+ this.clientId = clientId;
571
+ this.onConnect = options.onConnect;
572
+ this.onDisconnect = options.onDisconnect;
573
+ this.onTopicAnnounced = options.onTopicAnnounced;
574
+ this.onTopicRemoved = options.onTopicRemoved;
575
+ this.onTopicUpdated = options.onTopicUpdated;
576
+ this.onDataReceived = options.onDataReceived;
577
+ this.retryPolicy = options.retryPolicy ?? NT4Client.defaultRetryPolicy;
578
+ this.pingTimeoutOverride_ms = options.pingTimeoutMilliseconds;
579
+ this.dataChannel = new NT4WebSocketAsync(this.serverAddress, this.port, this.clientId, {
580
+ secure: options.secure,
581
+ onMessage: this.onChannelMessage,
582
+ onClose: this.onChannelClose
583
+ });
584
+ this.rttChannel = new NT4WebSocketAsync(this.serverAddress, this.port, this.clientId, {
585
+ rtt: true,
586
+ secure: options.secure,
587
+ onMessage: this.onChannelMessageRTT
588
+ });
589
+ }
590
+ /**
591
+ * Returns NT4 server network address the client is using.
592
+ */
593
+ serverAddress;
594
+ /**
595
+ * Returns current connection state.
596
+ */
597
+ get state() {
598
+ return this.connectionState;
599
+ }
600
+ /**
601
+ * Returns current network latency in microseconds if known.
602
+ */
603
+ get networkLatencyMicroseconds() {
604
+ return this.networkLatency_μs;
605
+ }
606
+ /**
607
+ * Returns server time in microseconds that is current or
608
+ * based on the specified client (local) time.
609
+ *
610
+ * Returns `null` if the server time offset is not established.
611
+ */
612
+ serverTimeMicroseconds(clientTimeMicroseconds) {
613
+ return this.serverTimeOffset_μs != null ? Math.round((clientTimeMicroseconds != null ? clientTimeMicroseconds : this.getClientTimestamp_μs()) + this.serverTimeOffset_μs) : null;
614
+ }
615
+ /**
616
+ * Initiates the connection.
617
+ * Once established the connection will be automatically kept alive by reconnecting.
618
+ */
619
+ connect() {
620
+ if (this.connectionInitiated) return;
621
+ Logger.Default.debug(`[NT4Client] connection initiated`);
622
+ this.connectionInitiated = true;
623
+ this.connectWithRetry();
624
+ }
625
+ /**
626
+ * Terminates the connection.
627
+ * The client can be reconnected manually by calling {@link connect} again.
628
+ */
629
+ disconnect() {
630
+ if (!this.connectionInitiated) return;
631
+ Logger.Default.debug(`[NT4Client] connection cancelled`);
632
+ this.connectionInitiated = false;
633
+ this.dataChannel.close();
634
+ this.rttChannel.close();
635
+ }
636
+ /**
637
+ * Creates a new subscription.
638
+ *
639
+ * The subscription can be created regardless of connectivity state.
640
+ * All client subscriptions are automatically restored once connection
641
+ * has been established. This is also true for reconnects following
642
+ * loss of connectivity or deliberate disconnects.
643
+ *
644
+ * Caller can {@link unsubscribe} by passing the return value of this call or
645
+ * invoke {@link unsubscribeAll} to remove all existing client subscriptions.
646
+ *
647
+ * @param topics topics or prefixes to include in the subscription
648
+ * @param options subscription options
649
+ * @returns A subscription instance
650
+ */
651
+ subscribe(topics, options) {
652
+ const subscription = new NT4Subscription(this.generateSafeUID(this.hasSubscriptionWithId), topics, options);
653
+ this.subscriptions.set(subscription.uid, subscription);
654
+ if (this.connectionEstablished) this.subscribeCore(subscription);
655
+ return subscription;
656
+ }
657
+ /**
658
+ * Unsubscribes an existing subscription.
659
+ *
660
+ * @param arg subscription or subscription identifier
661
+ * @returns `true` if the subscription was successfully unsubscribed, `false` if the subscription was not found
662
+ */
663
+ unsubscribe(arg) {
664
+ const subscription = typeof arg === "number" ? this.subscriptions.get(arg) : arg;
665
+ if (!subscription) return false;
666
+ this.subscriptions.delete(subscription.uid);
667
+ if (this.connectionEstablished) this.unsubscribeCore(subscription);
668
+ return true;
669
+ }
670
+ /**
671
+ * Unsubscribes all existing subscriptions.
672
+ */
673
+ unsubscribeAll() {
674
+ for (const subscription of this.subscriptions.values()) this.unsubscribe(subscription);
675
+ }
676
+ /**
677
+ * Publishes a topic with the specified name and type.
678
+ *
679
+ * This method registers this client as a publisher for the topic.
680
+ * It should be called before {@link setValue} or {@link setValueAt}
681
+ * can be used.
682
+ *
683
+ * Publishing a topic on a disconnected client is allowed.
684
+ * Client will automatically register such publishers once the connection
685
+ * has been established and send default or retained value as appropriate.
686
+ *
687
+ * @param topic topic name
688
+ * @param type data type
689
+ * @param properties properties
690
+ */
691
+ publishTopic(topic, type, properties) {
692
+ let publishedTopic = this.publishedTopics.get(topic);
693
+ if (publishedTopic) return publishedTopic;
694
+ const uid = this.generateSafeUID(this.hasPublishedTopicWithId);
695
+ publishedTopic = new NT4Topic(uid, topic, type, properties, uid);
696
+ this.publishedTopics.set(publishedTopic.name, publishedTopic);
697
+ if (this.connectionEstablished) this.publishCore(publishedTopic);
698
+ return publishedTopic;
699
+ }
700
+ /**
701
+ * Un-publishes previously published topic.
702
+ *
703
+ * @param topic topic name
704
+ * @returns `true` if the topic was successfully unpublished, `false` if the topic was not published
705
+ */
706
+ unpublishTopic(topic) {
707
+ const publishedTopic = this.publishedTopics.get(topic);
708
+ if (!publishedTopic) return false;
709
+ this.publishedTopics.delete(publishedTopic.name);
710
+ if (this.connectionEstablished) this.unpublishCore(publishedTopic);
711
+ return true;
712
+ }
713
+ /**
714
+ * Determines whether client already publishes the topic.
715
+ *
716
+ * @param topic topic name
717
+ */
718
+ isTopicPublished(topic) {
719
+ return this.publishedTopics.has(topic);
720
+ }
721
+ /**
722
+ * Sets properties for the specified topic.
723
+ *
724
+ * @param topic topic name
725
+ * @param properties new properties
726
+ */
727
+ setTopicProperties(topic, properties) {
728
+ const publishedTopic = this.publishedTopics.get(topic);
729
+ if (publishedTopic) publishedTopic.mergeProperties(properties);
730
+ const announcedTopic = this.announcedTopics.get(topic);
731
+ if (announcedTopic) announcedTopic.mergeProperties(properties);
732
+ if (this.connectionEstablished) this.setPropertiesCore(topic, properties);
733
+ }
734
+ /**
735
+ * Sets persistent flag for the topic.
736
+ *
737
+ * @param topic topic name
738
+ * @param value value to set
739
+ */
740
+ setTopicPersistent(topic, value) {
741
+ this.setTopicProperties(topic, { persistent: value });
742
+ }
743
+ /**
744
+ * Sets retained flag for the topic.
745
+ *
746
+ * @param topic topic name
747
+ * @param value value to set
748
+ */
749
+ setTopicRetained(topic, value) {
750
+ this.setTopicProperties(topic, { retained: value });
751
+ }
752
+ /**
753
+ * Sends a value to the published topic using the current time.
754
+ *
755
+ * This client must publish the topic with {@link publishTopic} first.
756
+ *
757
+ * @param topic topic name
758
+ * @param value value to send
759
+ */
760
+ setValue(topic, value) {
761
+ this.setValueAt(topic, value, this.serverTimeMicroseconds() ?? 1);
762
+ }
763
+ /**
764
+ * Sends a default ("weak") value to the published topic.
765
+ *
766
+ * This client must publish the topic with {@link publishTopic} first.
767
+ *
768
+ * @param topic topic name
769
+ * @param value value to send
770
+ */
771
+ setDefaultValue(topic, value) {
772
+ this.setValueAt(topic, value, 0);
773
+ }
774
+ /**
775
+ * Sends a value to the published topic using the specified timestamp.
776
+ *
777
+ * This client must publish the topic with {@link publishTopic} first.
778
+ *
779
+ * @param topic topic name
780
+ * @param value value to send
781
+ * @param timestamp timestamp
782
+ */
783
+ setValueAt(topic, value, timestamp) {
784
+ const publishedTopic = this.publishedTopics.get(topic);
785
+ if (!publishedTopic) throw new Error("Topic must be published first");
786
+ publishedTopic.updateRetainedValue(value, timestamp);
787
+ if (this.onDataReceived) {
788
+ const announcedTopic = this.announcedTopics.get(topic);
789
+ if (announcedTopic) this.onDataReceived(announcedTopic, value, timestamp);
790
+ }
791
+ if (this.connectionEstablished && this.serverTimeOffset_μs != null) this.sendValueCore(publishedTopic, value, timestamp);
792
+ }
793
+ /**
794
+ * Returns current ping timeout.
795
+ *
796
+ * @returns A value in milliseconds or 0 if the timeout has not been set yet.
797
+ */
798
+ get pingTimeoutMilliseconds() {
799
+ return this.pingTimeout_ms;
800
+ }
801
+ /**
802
+ * Sets ping timeout.
803
+ *
804
+ * @param value Timeout in milliseconds or `undefined` to use default protocol timeout value.
805
+ */
806
+ setPingTimeoutMilliseconds(value) {
807
+ Logger.Default.debug(value != null ? `[NT4Client] ping timeout is set to ${value.toFixed(0)}ms` : `[NT4Client] ping timeout is reset to protocol-based default`);
808
+ this.pingTimeoutOverride_ms = value;
809
+ if (value != null) this.pingTimeout_ms = value;
810
+ else this.pingTimeout_ms = this.dataChannel.protocol === NetworkSpecs.protocol41 ? NetworkSpecs.pingTimeout41 : NetworkSpecs.pingPeriod40;
811
+ }
812
+ };
813
+
814
+ //#endregion
815
+ export { NT4Client, NT4Subscription, NT4Topic };
816
+ //# sourceMappingURL=index.mjs.map