sen-ether-client 0.1.0 → 0.1.1
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/API.md +16 -1
- package/README.md +12 -0
- package/index.js +11 -1
- package/lib/sen.js +411 -51
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -45,14 +45,21 @@ Connection options:
|
|
|
45
45
|
- `timeout`: discovery and operation timeout in ms.
|
|
46
46
|
- `discoverySettleMs`: discovery settle time after the first process is found.
|
|
47
47
|
Defaults to `100`.
|
|
48
|
+
- `busDiscoverySettleMs`: max wait after a lightweight session connection while
|
|
49
|
+
bus announcements arrive. Defaults to at least `300`.
|
|
48
50
|
- `reconnect`: whether to reconnect and restart interests.
|
|
49
51
|
- `reconnectDelayMs`: delay between reconnect attempts.
|
|
50
|
-
- `maxReconnectAttempts`: maximum reconnect attempts.
|
|
52
|
+
- `maxReconnectAttempts`: maximum reconnect attempts. Defaults to `0`, which
|
|
53
|
+
means unlimited retries.
|
|
51
54
|
- `participantReadyTimeoutMs`: short non-fatal grace timeout for bus
|
|
52
55
|
participant acknowledgements. Defaults to `1000`.
|
|
53
56
|
- `socketKeepAlive`: enable TCP keepalive. Defaults to `true`.
|
|
54
57
|
- `socketIdleTimeoutMs`: optional TCP idle timeout. Defaults to `0` because
|
|
55
58
|
valid SEN connections can be quiet on TCP while bus data flows separately.
|
|
59
|
+
- `presenceTimeoutMs`: close and reconnect when the connected SEN process stops
|
|
60
|
+
announcing ether presence beams. Defaults to `5000`; set `0` to disable.
|
|
61
|
+
- `presenceCheckIntervalMs`: presence watchdog check interval. Defaults to
|
|
62
|
+
`1000`.
|
|
56
63
|
|
|
57
64
|
`Sen.connect()` uses multicast discovery. `sen-ether-client` reads this SEN environment
|
|
58
65
|
variable as its multicast default:
|
|
@@ -104,6 +111,7 @@ Main methods:
|
|
|
104
111
|
- `await sen.connect(options)`
|
|
105
112
|
- `await sen.interest(query, options)`
|
|
106
113
|
- `await sen.session(name)`
|
|
114
|
+
- `await sen.discoverBuses(options)`
|
|
107
115
|
- `sen.listSessions()`
|
|
108
116
|
- `sen.listBuses(options)`
|
|
109
117
|
- `await sen.bus(name, options)`
|
|
@@ -117,6 +125,9 @@ Session and bus navigation:
|
|
|
117
125
|
```js
|
|
118
126
|
const sen = await Sen.connect();
|
|
119
127
|
|
|
128
|
+
console.log(await sen.discoverBuses());
|
|
129
|
+
// [{ session: 'hmi', bus: 'diagnostics', qualified: 'hmi.diagnostics' }]
|
|
130
|
+
|
|
120
131
|
for (const sessionName of sen.listSessions()) {
|
|
121
132
|
const session = await sen.session(sessionName);
|
|
122
133
|
console.log(sessionName, session.listBuses());
|
|
@@ -125,6 +136,10 @@ for (const sessionName of sen.listSessions()) {
|
|
|
125
136
|
const diagnostics = await sen.session('hmi').then(hmi => hmi.bus('diagnostics'));
|
|
126
137
|
```
|
|
127
138
|
|
|
139
|
+
`discoverBuses()` does not create interests and does not join any SEN bus. It
|
|
140
|
+
does open a lightweight process connection per discovered session, because SEN
|
|
141
|
+
presence beams announce sessions/processes but not the bus list.
|
|
142
|
+
|
|
128
143
|
Main events:
|
|
129
144
|
|
|
130
145
|
- `connect`
|
package/README.md
CHANGED
|
@@ -84,6 +84,11 @@ const sen = await Sen.connect({
|
|
|
84
84
|
});
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Connected sessions are monitored through SEN ether presence beams. If the
|
|
88
|
+
remote process stops announcing itself for `presenceTimeoutMs` milliseconds
|
|
89
|
+
(default `5000`), the client closes the stale connection and restarts the
|
|
90
|
+
configured interests.
|
|
91
|
+
|
|
87
92
|
`sen-ether-client` can work with several SEN sessions from the same client. The session
|
|
88
93
|
is inferred from the query:
|
|
89
94
|
|
|
@@ -98,6 +103,8 @@ You can also navigate explicitly through sessions and buses:
|
|
|
98
103
|
const sen = await Sen.connect();
|
|
99
104
|
|
|
100
105
|
console.log(sen.listSessions());
|
|
106
|
+
console.log(await sen.discoverBuses());
|
|
107
|
+
// [{ session: 'hmi', bus: 'diagnostics', qualified: 'hmi.diagnostics' }]
|
|
101
108
|
|
|
102
109
|
const hmi = await sen.session('hmi');
|
|
103
110
|
console.log(hmi.listBuses());
|
|
@@ -106,6 +113,11 @@ const diagnostics = await hmi.bus('diagnostics');
|
|
|
106
113
|
const probe = await diagnostics.waitFor('EtherProbe');
|
|
107
114
|
```
|
|
108
115
|
|
|
116
|
+
`discoverBuses()` does not create interests and does not join any SEN bus. It
|
|
117
|
+
uses discovery to find sessions and opens lightweight process connections only
|
|
118
|
+
to read bus announcements. If buses are not announced immediately after the
|
|
119
|
+
process connection, it waits up to `busDiscoverySettleMs` milliseconds.
|
|
120
|
+
|
|
109
121
|
You can also connect to one explicit session:
|
|
110
122
|
|
|
111
123
|
```js
|
package/index.js
CHANGED
|
@@ -34,13 +34,16 @@
|
|
|
34
34
|
* @property {string} [app] Remote process appName substring filter.
|
|
35
35
|
* @property {number} [timeout=3000] Discovery and operation timeout in ms.
|
|
36
36
|
* @property {number} [discoverySettleMs=100] Discovery settle time after the first process is found.
|
|
37
|
+
* @property {number} [busDiscoverySettleMs=300] Max wait after lightweight session connect before reading bus announcements.
|
|
37
38
|
* @property {number} [participantReadyTimeoutMs=1000] Short grace timeout for non-fatal bus participant acknowledgements.
|
|
38
39
|
* @property {boolean} [reconnect=true] Reconnect and restart interests after disconnection.
|
|
39
40
|
* @property {number} [reconnectDelayMs=500] Delay between reconnect attempts.
|
|
40
|
-
* @property {number} [maxReconnectAttempts=
|
|
41
|
+
* @property {number} [maxReconnectAttempts=0] Maximum reconnect attempts. `0` means unlimited.
|
|
41
42
|
* @property {boolean} [socketKeepAlive=true] Enable TCP keepalive on SEN ether connections.
|
|
42
43
|
* @property {number} [socketKeepAliveInitialDelayMs=1000] TCP keepalive initial delay.
|
|
43
44
|
* @property {number} [socketIdleTimeoutMs=0] Optional transport idle timeout in ms. `0` disables it.
|
|
45
|
+
* @property {number} [presenceTimeoutMs=5000] Close and reconnect when the connected SEN process stops announcing presence beams. `0` disables it.
|
|
46
|
+
* @property {number} [presenceCheckIntervalMs=1000] Presence watchdog check interval in ms.
|
|
44
47
|
* @property {string} [interfaceAddress] Local interface address or interface name for multicast discovery.
|
|
45
48
|
* @property {object} [target] Already discovered/direct SEN target.
|
|
46
49
|
*/
|
|
@@ -64,6 +67,13 @@
|
|
|
64
67
|
* @property {boolean} [qualified=false] Return session-qualified bus names.
|
|
65
68
|
*/
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {object} SenBusSummary
|
|
72
|
+
* @property {string} session SEN session name.
|
|
73
|
+
* @property {string} bus Bus name local to the session.
|
|
74
|
+
* @property {string} qualified Session-qualified bus name usable in `SELECT * FROM <qualified>`.
|
|
75
|
+
*/
|
|
76
|
+
|
|
67
77
|
/**
|
|
68
78
|
* @typedef {string | number | ((object: SenRemoteObject) => boolean)} SenObjectSelector
|
|
69
79
|
*/
|
package/lib/sen.js
CHANGED
|
@@ -2,7 +2,7 @@ import { once } from 'node:events';
|
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { decodePropertyValues, decodeValue, encodeArguments, decodeArguments } from './values.js';
|
|
4
4
|
import { EtherClient } from './client.js';
|
|
5
|
-
import { scan, scanTcpDiscoveryHub } from './discovery.js';
|
|
5
|
+
import { EtherDiscoveryScanner, TcpDiscoveryHubScanner, scan, scanTcpDiscoveryHub } from './discovery.js';
|
|
6
6
|
import { methodHash } from './hash32.js';
|
|
7
7
|
|
|
8
8
|
function wait(ms) {
|
|
@@ -48,15 +48,24 @@ function queryBusName(sessionName, bus) {
|
|
|
48
48
|
return text.includes('.') || !session ? text : `${session}.${text}`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
|
|
51
|
+
function targetSessionName(target) {
|
|
52
|
+
return target?.session?.name ?? target?.info?.sessionName ?? '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function filterTargets(processes, options = {}) {
|
|
56
|
+
let candidates = [...processes];
|
|
53
57
|
if (options.session) {
|
|
54
|
-
candidates = candidates.filter(item => item
|
|
58
|
+
candidates = candidates.filter(item => targetSessionName(item) === options.session);
|
|
55
59
|
}
|
|
56
60
|
if (options.app) {
|
|
57
61
|
const app = String(options.app).toLowerCase();
|
|
58
62
|
candidates = candidates.filter(item => String(item.process?.appName || '').toLowerCase().includes(app));
|
|
59
63
|
}
|
|
64
|
+
return candidates;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findTarget(processes, options) {
|
|
68
|
+
const candidates = filterTargets(processes, options);
|
|
60
69
|
if (!candidates.length) {
|
|
61
70
|
return null;
|
|
62
71
|
}
|
|
@@ -107,6 +116,19 @@ function sessionNameFromBusName(busName) {
|
|
|
107
116
|
return idx > 0 ? text.slice(0, idx) : '';
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
function busSummary(sessionName, busName) {
|
|
120
|
+
const session = String(sessionName || '').trim();
|
|
121
|
+
const bus = etherBusName(session, busName);
|
|
122
|
+
if (!session || !bus) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
session,
|
|
127
|
+
bus,
|
|
128
|
+
qualified: queryBusName(session, bus)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
function selectorDescription(selector) {
|
|
111
133
|
return typeof selector === 'function' ? '<predicate>' : String(selector);
|
|
112
134
|
}
|
|
@@ -129,6 +151,24 @@ function normalizeTimestampNs(value) {
|
|
|
129
151
|
return typeof value === 'bigint' ? value : BigInt(value);
|
|
130
152
|
}
|
|
131
153
|
|
|
154
|
+
function stateRequestKey(interestId, objectId) {
|
|
155
|
+
return `${interestId >>> 0}:${objectId >>> 0}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function waitForSessionBuses(session, timeoutMs) {
|
|
159
|
+
let buses = session.listBuses();
|
|
160
|
+
if (buses.length || timeoutMs <= 0) {
|
|
161
|
+
return buses;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const deadline = Date.now() + timeoutMs;
|
|
165
|
+
while (!buses.length && Date.now() < deadline) {
|
|
166
|
+
await wait(Math.min(50, Math.max(1, deadline - Date.now())));
|
|
167
|
+
buses = session.listBuses();
|
|
168
|
+
}
|
|
169
|
+
return buses;
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
class ChangeBatcher {
|
|
133
173
|
constructor(interest, options = {}) {
|
|
134
174
|
this.interest = interest;
|
|
@@ -251,19 +291,40 @@ export class Sen extends EventEmitter {
|
|
|
251
291
|
return await sen.connect(options);
|
|
252
292
|
}
|
|
253
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Discover visible SEN buses without creating interests or joining buses.
|
|
296
|
+
*
|
|
297
|
+
* SEN discovery beams expose sessions/processes. Bus names are announced only
|
|
298
|
+
* after a lightweight process connection, so this method connects to each
|
|
299
|
+
* discovered session long enough to read its remote bus announcements.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} [options]
|
|
302
|
+
* @returns {Promise<Array<{session:string,bus:string,qualified:string}>>}
|
|
303
|
+
*/
|
|
304
|
+
static async discoverBuses(options = {}) {
|
|
305
|
+
const sen = new Sen(options);
|
|
306
|
+
try {
|
|
307
|
+
return await sen.discoverBuses(options);
|
|
308
|
+
} finally {
|
|
309
|
+
await sen.close().catch(() => {});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
254
313
|
constructor(options = {}) {
|
|
255
314
|
super();
|
|
256
315
|
this.options = {
|
|
257
316
|
appName: 'sen-ether-client',
|
|
258
317
|
reconnect: true,
|
|
259
318
|
reconnectDelayMs: 500,
|
|
260
|
-
maxReconnectAttempts:
|
|
319
|
+
maxReconnectAttempts: 0,
|
|
261
320
|
timeout: 3000,
|
|
262
321
|
discoverySettleMs: 100,
|
|
263
322
|
participantReadyTimeoutMs: 1000,
|
|
264
323
|
socketKeepAlive: true,
|
|
265
324
|
socketKeepAliveInitialDelayMs: 1000,
|
|
266
325
|
socketIdleTimeoutMs: 0,
|
|
326
|
+
presenceTimeoutMs: 5000,
|
|
327
|
+
presenceCheckIntervalMs: 1000,
|
|
267
328
|
...options
|
|
268
329
|
};
|
|
269
330
|
this.target = undefined;
|
|
@@ -271,9 +332,13 @@ export class Sen extends EventEmitter {
|
|
|
271
332
|
this.connectOptions = undefined;
|
|
272
333
|
this.manualClose = false;
|
|
273
334
|
this.reconnecting = false;
|
|
335
|
+
this.presenceScanner = undefined;
|
|
336
|
+
this.presenceTimer = undefined;
|
|
337
|
+
this.presenceLastSeen = 0;
|
|
274
338
|
this.remoteBuses = new Set();
|
|
275
339
|
this.buses = new Map();
|
|
276
340
|
this.sessions = new Map();
|
|
341
|
+
this.targets = [];
|
|
277
342
|
this.targetsBySession = new Map();
|
|
278
343
|
}
|
|
279
344
|
|
|
@@ -298,8 +363,9 @@ export class Sen extends EventEmitter {
|
|
|
298
363
|
if (!targets.length) {
|
|
299
364
|
throw new Error('no SEN ether processes discovered');
|
|
300
365
|
}
|
|
366
|
+
this.targets = targets;
|
|
301
367
|
for (const target of targets) {
|
|
302
|
-
const sessionName = target
|
|
368
|
+
const sessionName = targetSessionName(target);
|
|
303
369
|
if (sessionName && !this.targetsBySession.has(sessionName)) {
|
|
304
370
|
this.targetsBySession.set(sessionName, target);
|
|
305
371
|
}
|
|
@@ -324,6 +390,12 @@ export class Sen extends EventEmitter {
|
|
|
324
390
|
if (!sessionName) {
|
|
325
391
|
throw new Error('cannot connect without a SEN session name');
|
|
326
392
|
}
|
|
393
|
+
if (!this.targets.includes(target)) {
|
|
394
|
+
this.targets.push(target);
|
|
395
|
+
}
|
|
396
|
+
if (!this.targetsBySession.has(sessionName)) {
|
|
397
|
+
this.targetsBySession.set(sessionName, target);
|
|
398
|
+
}
|
|
327
399
|
|
|
328
400
|
const client = new EtherClient({
|
|
329
401
|
sessionName,
|
|
@@ -336,10 +408,20 @@ export class Sen extends EventEmitter {
|
|
|
336
408
|
this.target = target;
|
|
337
409
|
this.#wireClient(client);
|
|
338
410
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
411
|
+
try {
|
|
412
|
+
await client.connect(target);
|
|
413
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
414
|
+
this.#startPresenceWatchdog(target, config);
|
|
415
|
+
this.emit('connect', { target, sessionName });
|
|
416
|
+
return this;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
await client.close().catch(closeError => this.emit('warning', closeError));
|
|
419
|
+
if (this.client === client) {
|
|
420
|
+
this.client = undefined;
|
|
421
|
+
this.target = undefined;
|
|
422
|
+
}
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
343
425
|
}
|
|
344
426
|
|
|
345
427
|
/**
|
|
@@ -501,9 +583,10 @@ export class Sen extends EventEmitter {
|
|
|
501
583
|
}
|
|
502
584
|
|
|
503
585
|
return [...new Set([
|
|
586
|
+
...this.targets.map(targetSessionName),
|
|
504
587
|
...this.targetsBySession.keys(),
|
|
505
588
|
...this.sessions.keys()
|
|
506
|
-
])].sort();
|
|
589
|
+
].filter(Boolean))].sort();
|
|
507
590
|
}
|
|
508
591
|
|
|
509
592
|
listBuses(options = {}) {
|
|
@@ -519,6 +602,104 @@ export class Sen extends EventEmitter {
|
|
|
519
602
|
.map(busName => options.qualified ? queryBusName(sessionName, busName) : busName);
|
|
520
603
|
}
|
|
521
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Discover visible SEN buses without creating interests or joining buses.
|
|
607
|
+
*
|
|
608
|
+
* @param {object} [options]
|
|
609
|
+
* @param {string} [options.session] Optional session filter.
|
|
610
|
+
* @param {number} [options.busDiscoverySettleMs] Delay after lightweight session connect before reading announced buses.
|
|
611
|
+
* @returns {Promise<Array<{session:string,bus:string,qualified:string}>>}
|
|
612
|
+
*/
|
|
613
|
+
async discoverBuses(options = {}) {
|
|
614
|
+
const config = { ...this.options, ...options };
|
|
615
|
+
const settleMs = Math.max(0, Number(config.busDiscoverySettleMs ?? Math.max(config.discoverySettleMs ?? 100, 1000)) || 0);
|
|
616
|
+
|
|
617
|
+
if (this.client) {
|
|
618
|
+
const sessionName = this.target?.session?.name ?? this.client.processInfo.sessionName;
|
|
619
|
+
return (await waitForSessionBuses(this, settleMs))
|
|
620
|
+
.map(busName => busSummary(sessionName, busName))
|
|
621
|
+
.filter(Boolean)
|
|
622
|
+
.sort((a, b) => a.qualified.localeCompare(b.qualified));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!this.targets.length && !this.sessions.size) {
|
|
626
|
+
const targets = await this.#discoverTargets(config);
|
|
627
|
+
if (!targets.length) {
|
|
628
|
+
throw new Error('no SEN ether processes discovered');
|
|
629
|
+
}
|
|
630
|
+
this.targets = targets;
|
|
631
|
+
for (const target of targets) {
|
|
632
|
+
const sessionName = targetSessionName(target);
|
|
633
|
+
if (sessionName && !this.targetsBySession.has(sessionName)) {
|
|
634
|
+
this.targetsBySession.set(sessionName, target);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const summaries = new Map();
|
|
640
|
+
const addBus = (sessionName, busName) => {
|
|
641
|
+
const summary = busSummary(sessionName, busName);
|
|
642
|
+
if (summary) {
|
|
643
|
+
summaries.set(summary.qualified, summary);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
for (const session of this.sessions.values()) {
|
|
648
|
+
const sessionName = session.target?.session?.name ?? session.client?.processInfo?.sessionName;
|
|
649
|
+
for (const busName of await waitForSessionBuses(session, settleMs)) {
|
|
650
|
+
addBus(sessionName, busName);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const targets = filterTargets(this.targets, config);
|
|
655
|
+
const discoveries = targets.map(async target => {
|
|
656
|
+
const sessionName = targetSessionName(target);
|
|
657
|
+
if (!sessionName) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const session = new Sen({
|
|
662
|
+
...config,
|
|
663
|
+
session: sessionName,
|
|
664
|
+
reconnect: false
|
|
665
|
+
});
|
|
666
|
+
session.on('warning', error => this.emit('warning', error));
|
|
667
|
+
session.on('error', error => this.emit('warning', error));
|
|
668
|
+
try {
|
|
669
|
+
await session.connect({
|
|
670
|
+
...config,
|
|
671
|
+
session: sessionName,
|
|
672
|
+
target,
|
|
673
|
+
reconnect: false
|
|
674
|
+
});
|
|
675
|
+
for (const busName of await waitForSessionBuses(session, settleMs)) {
|
|
676
|
+
addBus(sessionName, busName);
|
|
677
|
+
}
|
|
678
|
+
} finally {
|
|
679
|
+
await session.close().catch(error => this.emit('warning', error));
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const results = await Promise.allSettled(discoveries);
|
|
684
|
+
const failures = [];
|
|
685
|
+
for (const result of results) {
|
|
686
|
+
if (result.status === 'rejected') {
|
|
687
|
+
failures.push(result.reason);
|
|
688
|
+
this.emit('warning', result.reason);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!summaries.size && targets.length && failures.length === targets.length) {
|
|
693
|
+
const sessions = [...new Set(targets.map(targetSessionName).filter(Boolean))].join(', ');
|
|
694
|
+
const error = new Error(`could not read SEN bus announcements from any discovered target${sessions ? ` in sessions: ${sessions}` : ''}`);
|
|
695
|
+
error.code = 'SEN_BUS_DISCOVERY_FAILED';
|
|
696
|
+
error.cause = failures[0];
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return [...summaries.values()].sort((a, b) => a.qualified.localeCompare(b.qualified));
|
|
701
|
+
}
|
|
702
|
+
|
|
522
703
|
objects() {
|
|
523
704
|
if (!this.client) {
|
|
524
705
|
return [...this.sessions.values()].flatMap(session => session.objects());
|
|
@@ -561,13 +742,14 @@ export class Sen extends EventEmitter {
|
|
|
561
742
|
async close() {
|
|
562
743
|
this.manualClose = true;
|
|
563
744
|
for (const session of this.sessions.values()) {
|
|
564
|
-
await session.close();
|
|
745
|
+
await session.close().catch(error => this.emit('warning', error));
|
|
565
746
|
}
|
|
566
747
|
for (const bus of this.buses.values()) {
|
|
567
748
|
bus.close();
|
|
568
749
|
}
|
|
750
|
+
this.#stopPresenceWatchdog();
|
|
569
751
|
await wait(50);
|
|
570
|
-
await this.client?.close();
|
|
752
|
+
await this.client?.close().catch(error => this.emit('warning', error));
|
|
571
753
|
this.client = undefined;
|
|
572
754
|
this.sessions.clear();
|
|
573
755
|
this.buses.clear();
|
|
@@ -675,9 +857,10 @@ export class Sen extends EventEmitter {
|
|
|
675
857
|
this.emit('error', error);
|
|
676
858
|
});
|
|
677
859
|
client.on('close', hadError => {
|
|
860
|
+
this.#stopPresenceWatchdog();
|
|
678
861
|
this.emit('close', hadError);
|
|
679
862
|
if (!this.manualClose && this.options.reconnect !== false) {
|
|
680
|
-
this.#reconnect().catch(error => this.emit('
|
|
863
|
+
this.#reconnect().catch(error => this.emit('warning', error));
|
|
681
864
|
}
|
|
682
865
|
});
|
|
683
866
|
}
|
|
@@ -688,12 +871,15 @@ export class Sen extends EventEmitter {
|
|
|
688
871
|
}
|
|
689
872
|
|
|
690
873
|
this.reconnecting = true;
|
|
874
|
+
this.#stopPresenceWatchdog();
|
|
691
875
|
await this.client?.close().catch(error => this.emit('warning', error));
|
|
692
876
|
this.emit('reconnecting');
|
|
693
|
-
const
|
|
877
|
+
const configuredMaxAttempts = this.connectOptions.maxReconnectAttempts ?? this.options.maxReconnectAttempts ?? 0;
|
|
878
|
+
const maxAttempts = Number(configuredMaxAttempts);
|
|
879
|
+
const unlimited = !Number.isFinite(maxAttempts) || maxAttempts <= 0;
|
|
694
880
|
const delayMs = this.connectOptions.reconnectDelayMs ?? this.options.reconnectDelayMs ?? 500;
|
|
695
881
|
|
|
696
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
882
|
+
for (let attempt = 1; unlimited || attempt <= maxAttempts; attempt += 1) {
|
|
697
883
|
let client;
|
|
698
884
|
try {
|
|
699
885
|
await wait(delayMs);
|
|
@@ -727,6 +913,7 @@ export class Sen extends EventEmitter {
|
|
|
727
913
|
|
|
728
914
|
await client.connect(target);
|
|
729
915
|
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
916
|
+
this.#startPresenceWatchdog(target, config);
|
|
730
917
|
|
|
731
918
|
for (const bus of this.buses.values()) {
|
|
732
919
|
await bus.rejoin(config.timeout ?? 3000);
|
|
@@ -737,6 +924,10 @@ export class Sen extends EventEmitter {
|
|
|
737
924
|
return;
|
|
738
925
|
} catch (error) {
|
|
739
926
|
await client?.close().catch(closeError => this.emit('warning', closeError));
|
|
927
|
+
if (this.client === client) {
|
|
928
|
+
this.client = undefined;
|
|
929
|
+
this.target = undefined;
|
|
930
|
+
}
|
|
740
931
|
if (this.manualClose) {
|
|
741
932
|
this.reconnecting = false;
|
|
742
933
|
return;
|
|
@@ -754,6 +945,74 @@ export class Sen extends EventEmitter {
|
|
|
754
945
|
throw new Error(`failed to reconnect SEN ether after ${maxAttempts} attempt(s)`);
|
|
755
946
|
}
|
|
756
947
|
|
|
948
|
+
#startPresenceWatchdog(target, config) {
|
|
949
|
+
this.#stopPresenceWatchdog();
|
|
950
|
+
const key = target?.key;
|
|
951
|
+
if (!key) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const timeoutMs = Number(config.presenceTimeoutMs ?? this.options.presenceTimeoutMs ?? 0);
|
|
956
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const intervalMs = Math.max(
|
|
961
|
+
250,
|
|
962
|
+
Number(config.presenceCheckIntervalMs ?? this.options.presenceCheckIntervalMs ?? 1000) || 1000
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
const scanner = config.tcpHub
|
|
966
|
+
? new TcpDiscoveryHubScanner(parseHostPort(config.tcpHub))
|
|
967
|
+
: new EtherDiscoveryScanner(config);
|
|
968
|
+
|
|
969
|
+
this.presenceScanner = scanner;
|
|
970
|
+
this.presenceLastSeen = Date.now();
|
|
971
|
+
|
|
972
|
+
scanner.on('beam', process => {
|
|
973
|
+
if (process.key === key) {
|
|
974
|
+
this.presenceLastSeen = Date.now();
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
scanner.on('error', error => this.emit('warning', error));
|
|
978
|
+
scanner.on('close', hadError => {
|
|
979
|
+
if (!this.manualClose && !this.reconnecting) {
|
|
980
|
+
this.emit('warning', new Error(`SEN ether discovery watchdog closed${hadError ? ' with error' : ''}`));
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
scanner.start().catch(error => this.emit('warning', error));
|
|
984
|
+
|
|
985
|
+
this.presenceTimer = setInterval(() => {
|
|
986
|
+
if (this.manualClose || this.reconnecting || !this.client) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const elapsedMs = Date.now() - this.presenceLastSeen;
|
|
990
|
+
if (elapsedMs <= timeoutMs) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const error = new Error(`SEN ether presence timeout after ${elapsedMs}ms without beam from ${key}`);
|
|
994
|
+
error.code = 'SEN_PRESENCE_TIMEOUT';
|
|
995
|
+
this.emit('warning', error);
|
|
996
|
+
this.client.socket?.destroy(error);
|
|
997
|
+
}, intervalMs);
|
|
998
|
+
this.presenceTimer.unref?.();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
#stopPresenceWatchdog() {
|
|
1002
|
+
if (this.presenceTimer) {
|
|
1003
|
+
clearInterval(this.presenceTimer);
|
|
1004
|
+
this.presenceTimer = undefined;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const scanner = this.presenceScanner;
|
|
1008
|
+
this.presenceScanner = undefined;
|
|
1009
|
+
this.presenceLastSeen = 0;
|
|
1010
|
+
if (scanner) {
|
|
1011
|
+
scanner.removeAllListeners();
|
|
1012
|
+
scanner.stop().catch(error => this.emit('warning', error));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
757
1016
|
#busForEvent(event) {
|
|
758
1017
|
if (event.bus?.busName) {
|
|
759
1018
|
return this.buses.get(event.bus.busName);
|
|
@@ -814,18 +1073,29 @@ export class SenBus extends EventEmitter {
|
|
|
814
1073
|
const interestId = typeof id === 'object' ? id.id : id;
|
|
815
1074
|
this.sen.client.stopInterest(this.name, interestId);
|
|
816
1075
|
const interest = this.interests.get(interestId);
|
|
1076
|
+
this.#detachInterestObjects(interestId, interest);
|
|
817
1077
|
this.interests.delete(interestId);
|
|
818
1078
|
interest?.closeLocal();
|
|
819
1079
|
interest?.emit('close');
|
|
820
1080
|
}
|
|
821
1081
|
|
|
822
1082
|
close() {
|
|
823
|
-
for (const interest of this.interests.values()) {
|
|
824
|
-
|
|
825
|
-
|
|
1083
|
+
for (const interest of [...this.interests.values()]) {
|
|
1084
|
+
try {
|
|
1085
|
+
this.stopInterest(interest.id);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
this.#detachInterestObjects(interest.id, interest);
|
|
1088
|
+
this.interests.delete(interest.id);
|
|
1089
|
+
interest.closeLocal();
|
|
1090
|
+
interest.emit('close');
|
|
1091
|
+
this.sen.emit('warning', error);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
this.sen.client.leaveBus(this.name);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
this.sen.emit('warning', error);
|
|
826
1098
|
}
|
|
827
|
-
this.interests.clear();
|
|
828
|
-
this.sen.client.leaveBus(this.name);
|
|
829
1099
|
}
|
|
830
1100
|
|
|
831
1101
|
prepareReconnect() {
|
|
@@ -884,24 +1154,36 @@ export class SenBus extends EventEmitter {
|
|
|
884
1154
|
const newTypeHashes = new Set();
|
|
885
1155
|
for (const discovery of event.discoveries ?? []) {
|
|
886
1156
|
for (const info of discovery.objects ?? []) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1157
|
+
let object = this.objectsById.get(info.id);
|
|
1158
|
+
const isNewObject = !object;
|
|
1159
|
+
if (!object) {
|
|
1160
|
+
object = new SenRemoteObject(this, {
|
|
1161
|
+
...info,
|
|
1162
|
+
ownerId: event.ownerId,
|
|
1163
|
+
interestId: discovery.interestId
|
|
1164
|
+
});
|
|
1165
|
+
this.objectsById.set(object.id, object);
|
|
1166
|
+
} else {
|
|
1167
|
+
object.attachInterest(discovery.interestId);
|
|
1168
|
+
object.updateDiscoveryInfo({
|
|
1169
|
+
...info,
|
|
1170
|
+
ownerId: event.ownerId
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
893
1173
|
const interest = this.interests.get(discovery.interestId);
|
|
894
1174
|
interest?.objectsById.set(object.id, object);
|
|
895
1175
|
if (info.state?.length) {
|
|
896
|
-
object.applyState(info.state, 'state', info.time);
|
|
1176
|
+
object.applyState(info.state, 'state', info.time, { interestId: discovery.interestId });
|
|
897
1177
|
}
|
|
898
1178
|
if (!this.requestedTypeHashes.has(info.typeHash)) {
|
|
899
1179
|
this.requestedTypeHashes.add(info.typeHash);
|
|
900
1180
|
newTypeHashes.add(info.typeHash);
|
|
901
1181
|
}
|
|
902
1182
|
interest?.emit('object', object);
|
|
903
|
-
|
|
904
|
-
|
|
1183
|
+
if (isNewObject) {
|
|
1184
|
+
this.emit('object', object);
|
|
1185
|
+
this.sen.emit('object', object);
|
|
1186
|
+
}
|
|
905
1187
|
}
|
|
906
1188
|
}
|
|
907
1189
|
if (newTypeHashes.size) {
|
|
@@ -914,20 +1196,47 @@ export class SenBus extends EventEmitter {
|
|
|
914
1196
|
for (const removal of event.removals ?? []) {
|
|
915
1197
|
for (const id of removal.ids ?? []) {
|
|
916
1198
|
const object = this.objectsById.get(id);
|
|
917
|
-
this.objectsById.delete(id);
|
|
918
|
-
this.stateRequestedObjectIds.delete(id);
|
|
919
1199
|
const interest = this.interests.get(removal.interestId);
|
|
920
1200
|
interest?.objectsById.delete(id);
|
|
1201
|
+
this.stateRequestedObjectIds.delete(stateRequestKey(removal.interestId, id));
|
|
921
1202
|
if (object) {
|
|
922
|
-
object.
|
|
1203
|
+
object.detachInterest(removal.interestId);
|
|
1204
|
+
object.emit('remove', { interestId: removal.interestId });
|
|
923
1205
|
interest?.emit('remove', object);
|
|
924
|
-
|
|
925
|
-
|
|
1206
|
+
if (object.interestIds.size === 0) {
|
|
1207
|
+
this.objectsById.delete(id);
|
|
1208
|
+
this.requestedTypeHashes.delete(object.typeHash);
|
|
1209
|
+
this.emit('remove', object);
|
|
1210
|
+
this.sen.emit('remove', object);
|
|
1211
|
+
}
|
|
926
1212
|
}
|
|
927
1213
|
}
|
|
928
1214
|
}
|
|
929
1215
|
}
|
|
930
1216
|
|
|
1217
|
+
#detachInterestObjects(interestId, interest) {
|
|
1218
|
+
const normalizedInterestId = interestId >>> 0;
|
|
1219
|
+
const keyPrefix = `${normalizedInterestId}:`;
|
|
1220
|
+
for (const key of [...this.stateRequestedObjectIds]) {
|
|
1221
|
+
if (key.startsWith(keyPrefix)) {
|
|
1222
|
+
this.stateRequestedObjectIds.delete(key);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (!interest) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
for (const object of interest.objectsById.values()) {
|
|
1231
|
+
object.detachInterest(normalizedInterestId);
|
|
1232
|
+
if (object.interestIds.size === 0) {
|
|
1233
|
+
this.objectsById.delete(object.id);
|
|
1234
|
+
this.requestedTypeHashes.delete(object.typeHash);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
interest.objectsById.clear();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
931
1240
|
handleTypesInfoResponse(event) {
|
|
932
1241
|
const dependentTypeHashes = new Set();
|
|
933
1242
|
for (const type of event.types ?? []) {
|
|
@@ -962,7 +1271,7 @@ export class SenBus extends EventEmitter {
|
|
|
962
1271
|
if (!object) {
|
|
963
1272
|
continue;
|
|
964
1273
|
}
|
|
965
|
-
object.applyState(state.state, 'state', state.timestamp);
|
|
1274
|
+
object.applyState(state.state, 'state', state.timestamp, { interestId: response.interestId });
|
|
966
1275
|
}
|
|
967
1276
|
}
|
|
968
1277
|
}
|
|
@@ -1042,14 +1351,20 @@ export class SenBus extends EventEmitter {
|
|
|
1042
1351
|
|
|
1043
1352
|
#requestReadyObjectStates() {
|
|
1044
1353
|
const requestsByInterest = new Map();
|
|
1045
|
-
for (const
|
|
1046
|
-
|
|
1047
|
-
|
|
1354
|
+
for (const interest of this.interests.values()) {
|
|
1355
|
+
for (const object of interest.objectsById.values()) {
|
|
1356
|
+
if (!object.spec) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const key = stateRequestKey(interest.id, object.id);
|
|
1360
|
+
if (this.stateRequestedObjectIds.has(key)) {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
this.stateRequestedObjectIds.add(key);
|
|
1364
|
+
const ids = requestsByInterest.get(interest.id) ?? [];
|
|
1365
|
+
ids.push(object.id);
|
|
1366
|
+
requestsByInterest.set(interest.id, ids);
|
|
1048
1367
|
}
|
|
1049
|
-
this.stateRequestedObjectIds.add(object.id);
|
|
1050
|
-
const ids = requestsByInterest.get(object.interestId) ?? [];
|
|
1051
|
-
ids.push(object.id);
|
|
1052
|
-
requestsByInterest.set(object.interestId, ids);
|
|
1053
1368
|
}
|
|
1054
1369
|
|
|
1055
1370
|
if (requestsByInterest.size) {
|
|
@@ -1066,7 +1381,8 @@ export class SenBus extends EventEmitter {
|
|
|
1066
1381
|
object.applyState(
|
|
1067
1382
|
object.pendingState.buffer,
|
|
1068
1383
|
object.pendingState.source,
|
|
1069
|
-
object.pendingState.timestampNs
|
|
1384
|
+
object.pendingState.timestampNs,
|
|
1385
|
+
{ interestId: object.pendingState.interestId }
|
|
1070
1386
|
);
|
|
1071
1387
|
}
|
|
1072
1388
|
}
|
|
@@ -1164,6 +1480,10 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1164
1480
|
this.typeHash = info.typeHash;
|
|
1165
1481
|
this.ownerId = info.ownerId;
|
|
1166
1482
|
this.interestId = info.interestId;
|
|
1483
|
+
this.interestIds = new Set();
|
|
1484
|
+
if (info.interestId !== undefined) {
|
|
1485
|
+
this.interestIds.add(info.interestId);
|
|
1486
|
+
}
|
|
1167
1487
|
this.snapshot = {};
|
|
1168
1488
|
this.spec = undefined;
|
|
1169
1489
|
this.pendingState = undefined;
|
|
@@ -1186,6 +1506,29 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1186
1506
|
return this.name === selector || this.className === selector || String(this.id) === String(selector);
|
|
1187
1507
|
}
|
|
1188
1508
|
|
|
1509
|
+
attachInterest(interestId) {
|
|
1510
|
+
if (interestId !== undefined) {
|
|
1511
|
+
this.interestIds.add(interestId);
|
|
1512
|
+
this.interestId = interestId;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
detachInterest(interestId) {
|
|
1517
|
+
if (interestId !== undefined) {
|
|
1518
|
+
this.interestIds.delete(interestId);
|
|
1519
|
+
if (this.interestId === interestId) {
|
|
1520
|
+
this.interestId = this.interestIds.values().next().value;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
updateDiscoveryInfo(info) {
|
|
1526
|
+
this.name = info.name ?? this.name;
|
|
1527
|
+
this.className = info.className ?? this.className;
|
|
1528
|
+
this.typeHash = info.typeHash ?? this.typeHash;
|
|
1529
|
+
this.ownerId = info.ownerId ?? this.ownerId;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1189
1532
|
property(name) {
|
|
1190
1533
|
return findByName(collectClassMembers(this.spec, this.bus.typeRegistry, 'properties'), name);
|
|
1191
1534
|
}
|
|
@@ -1259,17 +1602,18 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1259
1602
|
return await this.bus.callObjectMethod(this, method, args, options);
|
|
1260
1603
|
}
|
|
1261
1604
|
|
|
1262
|
-
applyState(buffer, source, timestamp) {
|
|
1605
|
+
applyState(buffer, source, timestamp, options = {}) {
|
|
1263
1606
|
const timestampNs = normalizeTimestampNs(timestamp);
|
|
1264
1607
|
this.#rememberObjectTimestamp(source, timestampNs);
|
|
1265
1608
|
|
|
1266
1609
|
if (!this.spec) {
|
|
1267
|
-
this.pendingState = { buffer, source, timestampNs };
|
|
1610
|
+
this.pendingState = { buffer, source, timestampNs, interestId: options.interestId };
|
|
1268
1611
|
return;
|
|
1269
1612
|
}
|
|
1270
1613
|
|
|
1271
|
-
const
|
|
1272
|
-
const
|
|
1614
|
+
const interests = this.#targetInterests(options.interestId);
|
|
1615
|
+
const decodeInterest = options.interestId !== undefined || interests.length === 1 ? interests[0] : undefined;
|
|
1616
|
+
const values = decodePropertyValues(buffer, this.spec, this.bus.typeRegistry, decodeInterest?.decodeOptions());
|
|
1273
1617
|
let complete = true;
|
|
1274
1618
|
for (const value of values) {
|
|
1275
1619
|
if (!value.decoded) {
|
|
@@ -1292,9 +1636,10 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1292
1636
|
previous,
|
|
1293
1637
|
property: value.property
|
|
1294
1638
|
};
|
|
1295
|
-
|
|
1639
|
+
for (const interest of interests) {
|
|
1296
1640
|
interest.publishChange(change);
|
|
1297
|
-
}
|
|
1641
|
+
}
|
|
1642
|
+
if (!interests.length) {
|
|
1298
1643
|
this.emit('change', change);
|
|
1299
1644
|
this.emit(`change:${value.name}`, change);
|
|
1300
1645
|
this.bus.emit('change', change);
|
|
@@ -1302,7 +1647,22 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1302
1647
|
}
|
|
1303
1648
|
}
|
|
1304
1649
|
|
|
1305
|
-
this.pendingState = complete ? undefined : { buffer, source, timestampNs };
|
|
1650
|
+
this.pendingState = complete ? undefined : { buffer, source, timestampNs, interestId: options.interestId };
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
#targetInterests(interestId) {
|
|
1654
|
+
if (interestId !== undefined) {
|
|
1655
|
+
const interest = this.bus.interests.get(interestId);
|
|
1656
|
+
return interest ? [interest] : [];
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const interests = [];
|
|
1660
|
+
for (const interest of this.bus.interests.values()) {
|
|
1661
|
+
if (interest.objectsById.has(this.id)) {
|
|
1662
|
+
interests.push(interest);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return interests;
|
|
1306
1666
|
}
|
|
1307
1667
|
|
|
1308
1668
|
#rememberObjectTimestamp(source, timestampNs) {
|