sen-ether-client 0.1.0 → 0.1.2
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/bin/node-sen-probe.js +4 -2
- package/index.js +11 -1
- package/lib/client.js +212 -4
- package/lib/sen.js +420 -55
- package/lib/values.js +1 -3
- 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/bin/node-sen-probe.js
CHANGED
|
@@ -254,7 +254,9 @@ try {
|
|
|
254
254
|
|
|
255
255
|
const client = new EtherClient({
|
|
256
256
|
sessionName: target.session.name,
|
|
257
|
-
appName: 'sen-ether-probe'
|
|
257
|
+
appName: 'sen-ether-probe',
|
|
258
|
+
interfaceAddress: options.interfaceAddress,
|
|
259
|
+
discoveryPort: options.port
|
|
258
260
|
});
|
|
259
261
|
const remoteBuses = new Set();
|
|
260
262
|
const requestedTypeHashes = new Set();
|
|
@@ -405,7 +407,7 @@ try {
|
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
|
|
408
|
-
client.joinBus(bus);
|
|
410
|
+
await client.joinBus(bus);
|
|
409
411
|
await waitForEvent(client, 'busParticipantReady', 5000).catch(error => {
|
|
410
412
|
console.warn(`[warn] ${error.message}; starting interest anyway`);
|
|
411
413
|
});
|
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/client.js
CHANGED
|
@@ -24,6 +24,18 @@ import { crc32 } from './crc32.js';
|
|
|
24
24
|
|
|
25
25
|
const LINUX_OS_KIND = 1;
|
|
26
26
|
const X64_CPU_ARCH = 1;
|
|
27
|
+
const DEFAULT_DISCOVERY_PORT = 60543;
|
|
28
|
+
const DEFAULT_BUS_MULTICAST_PORT = 50985;
|
|
29
|
+
const BUS_HASH_SEED = 15071983;
|
|
30
|
+
const FNV1A_OFFSET_BASIS = 0x811c9dc5;
|
|
31
|
+
const FNV1A_PRIME = 0x01000193;
|
|
32
|
+
const HASH_COMBINE_MAGIC = 0x9e3779b9;
|
|
33
|
+
const DEFAULT_MULTICAST_RANGE = Object.freeze([
|
|
34
|
+
{ min: 224, max: 239 },
|
|
35
|
+
{ min: 0, max: 255 },
|
|
36
|
+
{ min: 0, max: 255 },
|
|
37
|
+
{ min: 0, max: 255 }
|
|
38
|
+
]);
|
|
27
39
|
|
|
28
40
|
function randomUInt32() {
|
|
29
41
|
return randomBytes(4).readUInt32LE(0);
|
|
@@ -57,6 +69,87 @@ function detectCpuArch() {
|
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
function discoveryPortFromEnv() {
|
|
73
|
+
const value = process.env.SEN_ETHER_DISCOVERY_PORT;
|
|
74
|
+
if (!value) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const port = Number(value);
|
|
78
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
79
|
+
throw new Error(`invalid SEN discovery port in environment: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
return port;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveInterfaceAddress(value) {
|
|
85
|
+
const text = String(value || '').trim();
|
|
86
|
+
if (!text) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(text)) {
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const interfaces = os.networkInterfaces();
|
|
94
|
+
const candidates = interfaces[text];
|
|
95
|
+
if (!candidates) {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ipv4 = candidates.find(item => (item.family === 'IPv4' || item.family === 4) && !item.internal);
|
|
100
|
+
if (!ipv4) {
|
|
101
|
+
throw new Error(`network interface "${text}" has no non-internal IPv4 address`);
|
|
102
|
+
}
|
|
103
|
+
return ipv4.address;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeMulticastRange(value) {
|
|
107
|
+
const ranges = Array.isArray(value) && value.length === 4 ? value : DEFAULT_MULTICAST_RANGE;
|
|
108
|
+
return ranges.map((range, index) => {
|
|
109
|
+
const fallback = DEFAULT_MULTICAST_RANGE[index];
|
|
110
|
+
const min = Number(range?.min ?? fallback.min);
|
|
111
|
+
const max = Number(range?.max ?? fallback.max);
|
|
112
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max > 255 || min > max) {
|
|
113
|
+
throw new Error(`invalid SEN bus multicast range at byte ${index}`);
|
|
114
|
+
}
|
|
115
|
+
return { min, max };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function computeByte(range, hashByte) {
|
|
120
|
+
const length = range.max - range.min;
|
|
121
|
+
return length !== 0 ? range.min + (hashByte % length) : range.min;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hashIntegral(value, byteSize) {
|
|
125
|
+
let hash = FNV1A_OFFSET_BASIS;
|
|
126
|
+
for (let shift = (byteSize - 1) * 8; shift >= 0; shift -= 8) {
|
|
127
|
+
hash ^= (value >>> shift) & 0xff;
|
|
128
|
+
hash = Math.imul(hash, FNV1A_PRIME) >>> 0;
|
|
129
|
+
}
|
|
130
|
+
return hash >>> 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function combineHashed(seed, hashed) {
|
|
134
|
+
return (seed ^ (
|
|
135
|
+
(hashed + HASH_COMBINE_MAGIC + ((seed << 6) >>> 0) + (seed >>> 2)) >>> 0
|
|
136
|
+
)) >>> 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function computeBusMulticastGroup(sessionId, busId, discoveryPort, ranges) {
|
|
140
|
+
let hash = BUS_HASH_SEED;
|
|
141
|
+
hash = combineHashed(hash, hashIntegral(sessionId >>> 0, 4));
|
|
142
|
+
hash = combineHashed(hash, hashIntegral(busId >>> 0, 4));
|
|
143
|
+
hash = combineHashed(hash, hashIntegral(discoveryPort >>> 0, 2));
|
|
144
|
+
const bytes = [
|
|
145
|
+
computeByte(ranges[0], hash & 0xff),
|
|
146
|
+
computeByte(ranges[1], (hash >>> 8) & 0xff),
|
|
147
|
+
computeByte(ranges[2], (hash >>> 16) & 0xff),
|
|
148
|
+
computeByte(ranges[3], (hash >>> 24) & 0xff)
|
|
149
|
+
];
|
|
150
|
+
return bytes.map((byte, index) => Math.min(Math.max(byte, ranges[index].min), ranges[index].max)).join('.');
|
|
151
|
+
}
|
|
152
|
+
|
|
60
153
|
/**
|
|
61
154
|
* Create a ProcessInfo compatible with sen::kernel::getOwnProcessInfo.
|
|
62
155
|
*
|
|
@@ -139,9 +232,14 @@ export class EtherClient extends EventEmitter {
|
|
|
139
232
|
socketKeepAlive: true,
|
|
140
233
|
socketKeepAliveInitialDelayMs: 1000,
|
|
141
234
|
socketIdleTimeoutMs: 0,
|
|
235
|
+
discoveryPort: discoveryPortFromEnv() ?? DEFAULT_DISCOVERY_PORT,
|
|
236
|
+
busMulticastPort: DEFAULT_BUS_MULTICAST_PORT,
|
|
237
|
+
busMulticastRange: DEFAULT_MULTICAST_RANGE,
|
|
142
238
|
...options
|
|
143
239
|
};
|
|
144
240
|
this.processInfo = createProcessInfo(this.options);
|
|
241
|
+
this.interfaceAddress = resolveInterfaceAddress(this.options.interfaceAddress);
|
|
242
|
+
this.busMulticastRange = normalizeMulticastRange(this.options.busMulticastRange);
|
|
145
243
|
this.socket = undefined;
|
|
146
244
|
this.udpSocket = undefined;
|
|
147
245
|
this.receiveBuffer = Buffer.alloc(0);
|
|
@@ -242,6 +340,14 @@ export class EtherClient extends EventEmitter {
|
|
|
242
340
|
udpSocket.close(resolve);
|
|
243
341
|
}));
|
|
244
342
|
}
|
|
343
|
+
for (const busState of this.buses.values()) {
|
|
344
|
+
if (busState.multicastSocket) {
|
|
345
|
+
closing.push(new Promise(resolve => {
|
|
346
|
+
busState.multicastSocket.close(resolve);
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.buses.clear();
|
|
245
351
|
|
|
246
352
|
await Promise.allSettled(closing);
|
|
247
353
|
}
|
|
@@ -252,7 +358,7 @@ export class EtherClient extends EventEmitter {
|
|
|
252
358
|
* @param {string} busName
|
|
253
359
|
* @param {{ participantId?: number }} [options]
|
|
254
360
|
*/
|
|
255
|
-
joinBus(busName, options = {}) {
|
|
361
|
+
async joinBus(busName, options = {}) {
|
|
256
362
|
if (!this.socket) {
|
|
257
363
|
throw new Error('EtherClient is not connected');
|
|
258
364
|
}
|
|
@@ -264,10 +370,21 @@ export class EtherClient extends EventEmitter {
|
|
|
264
370
|
busId,
|
|
265
371
|
participantId,
|
|
266
372
|
readyRemoteParticipants: new Set(),
|
|
267
|
-
interests: new Map()
|
|
373
|
+
interests: new Map(),
|
|
374
|
+
multicastSocket: undefined,
|
|
375
|
+
multicastGroup: undefined
|
|
268
376
|
};
|
|
269
377
|
|
|
270
378
|
this.buses.set(busId, bus);
|
|
379
|
+
try {
|
|
380
|
+
if (this.options.busMulticast !== false) {
|
|
381
|
+
await this.#openBusMulticastSocket(bus);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.buses.delete(busId);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
|
|
271
388
|
this.#sendControlPayload(encodeEtherControlMessage({
|
|
272
389
|
type: 'BusJoined',
|
|
273
390
|
value: {
|
|
@@ -277,8 +394,20 @@ export class EtherClient extends EventEmitter {
|
|
|
277
394
|
}
|
|
278
395
|
}));
|
|
279
396
|
|
|
280
|
-
this.emit('busJoinedLocal', {
|
|
281
|
-
|
|
397
|
+
this.emit('busJoinedLocal', {
|
|
398
|
+
busName,
|
|
399
|
+
busId,
|
|
400
|
+
participantId,
|
|
401
|
+
multicastGroup: bus.multicastGroup,
|
|
402
|
+
multicastPort: this.options.busMulticastPort
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
busName,
|
|
406
|
+
busId,
|
|
407
|
+
participantId,
|
|
408
|
+
multicastGroup: bus.multicastGroup,
|
|
409
|
+
multicastPort: this.options.busMulticastPort
|
|
410
|
+
};
|
|
282
411
|
}
|
|
283
412
|
|
|
284
413
|
/**
|
|
@@ -426,6 +555,10 @@ export class EtherClient extends EventEmitter {
|
|
|
426
555
|
}
|
|
427
556
|
}));
|
|
428
557
|
this.buses.delete(busState.busId);
|
|
558
|
+
if (busState.multicastSocket) {
|
|
559
|
+
busState.multicastSocket.close();
|
|
560
|
+
busState.multicastSocket = undefined;
|
|
561
|
+
}
|
|
429
562
|
this.emit('busLeftLocal', {
|
|
430
563
|
busName: busState.busName,
|
|
431
564
|
busId: busState.busId,
|
|
@@ -573,6 +706,81 @@ export class EtherClient extends EventEmitter {
|
|
|
573
706
|
}
|
|
574
707
|
}
|
|
575
708
|
|
|
709
|
+
#onMulticastBusDatagram(busState, message, remote) {
|
|
710
|
+
try {
|
|
711
|
+
if (message.length < 8) {
|
|
712
|
+
throw new RangeError(`SEN multicast bus datagram too small: ${message.length}`);
|
|
713
|
+
}
|
|
714
|
+
const processId = message.readUInt32LE(0);
|
|
715
|
+
const payloadSize = message.readUInt32LE(4);
|
|
716
|
+
if (processId === this.processInfo.processId) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (payloadSize !== message.length - 8) {
|
|
720
|
+
throw new RangeError(
|
|
721
|
+
`SEN multicast bus payload size mismatch: expected ${payloadSize}, got ${message.length - 8}`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const frame = {
|
|
726
|
+
to: busState.participantId,
|
|
727
|
+
busId: busState.busId,
|
|
728
|
+
message: message.subarray(8)
|
|
729
|
+
};
|
|
730
|
+
const busMessage = decodeBusMessage(frame.message);
|
|
731
|
+
this.emit('busFrame', { ...frame, busMessage, remote, multicast: true });
|
|
732
|
+
if (busMessage.categoryName === 'controlMessage') {
|
|
733
|
+
this.emit('busControlMessage', { ...frame, control: busMessage.control, remote, multicast: true });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
this.emit(busMessage.categoryName, { ...frame, ...busMessage, remote, multicast: true });
|
|
737
|
+
} catch (error) {
|
|
738
|
+
this.emit('error', error);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async #openBusMulticastSocket(busState) {
|
|
743
|
+
const group = computeBusMulticastGroup(
|
|
744
|
+
this.processInfo.sessionId,
|
|
745
|
+
busState.busId,
|
|
746
|
+
this.options.discoveryPort,
|
|
747
|
+
this.busMulticastRange
|
|
748
|
+
);
|
|
749
|
+
const port = this.options.busMulticastPort;
|
|
750
|
+
const bindAddress = process.platform === 'win32' ? undefined : group;
|
|
751
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
752
|
+
busState.multicastSocket = socket;
|
|
753
|
+
busState.multicastGroup = group;
|
|
754
|
+
|
|
755
|
+
socket.on('message', (message, remote) => this.#onMulticastBusDatagram(busState, message, remote));
|
|
756
|
+
socket.on('error', error => this.emit('error', error));
|
|
757
|
+
|
|
758
|
+
await new Promise((resolve, reject) => {
|
|
759
|
+
const onError = error => {
|
|
760
|
+
socket.off('listening', onListening);
|
|
761
|
+
reject(error);
|
|
762
|
+
};
|
|
763
|
+
const onListening = () => {
|
|
764
|
+
socket.off('error', onError);
|
|
765
|
+
try {
|
|
766
|
+
socket.addMembership(group, this.interfaceAddress);
|
|
767
|
+
socket.setMulticastLoopback(true);
|
|
768
|
+
if (this.interfaceAddress) {
|
|
769
|
+
socket.setMulticastInterface(this.interfaceAddress);
|
|
770
|
+
}
|
|
771
|
+
} catch (error) {
|
|
772
|
+
reject(error);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
resolve();
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
socket.once('error', onError);
|
|
779
|
+
socket.once('listening', onListening);
|
|
780
|
+
socket.bind(port, bindAddress);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
576
784
|
#onRemoteParticipantReady(busState, frame, value) {
|
|
577
785
|
if (value.id !== busState.participantId) {
|
|
578
786
|
return;
|
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,22 +390,43 @@ 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,
|
|
330
402
|
appName: config.appName,
|
|
331
403
|
socketKeepAlive: config.socketKeepAlive,
|
|
332
404
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
333
|
-
socketIdleTimeoutMs: config.socketIdleTimeoutMs
|
|
405
|
+
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
406
|
+
interfaceAddress: config.interfaceAddress,
|
|
407
|
+
discoveryPort: config.port,
|
|
408
|
+
busMulticast: config.busMulticast,
|
|
409
|
+
busMulticastPort: config.busMulticastPort,
|
|
410
|
+
busMulticastRange: config.busMulticastRange
|
|
334
411
|
});
|
|
335
412
|
this.client = client;
|
|
336
413
|
this.target = target;
|
|
337
414
|
this.#wireClient(client);
|
|
338
415
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
416
|
+
try {
|
|
417
|
+
await client.connect(target);
|
|
418
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
419
|
+
this.#startPresenceWatchdog(target, config);
|
|
420
|
+
this.emit('connect', { target, sessionName });
|
|
421
|
+
return this;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await client.close().catch(closeError => this.emit('warning', closeError));
|
|
424
|
+
if (this.client === client) {
|
|
425
|
+
this.client = undefined;
|
|
426
|
+
this.target = undefined;
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
343
430
|
}
|
|
344
431
|
|
|
345
432
|
/**
|
|
@@ -375,7 +462,7 @@ export class Sen extends EventEmitter {
|
|
|
375
462
|
|
|
376
463
|
let senBus = this.buses.get(bus);
|
|
377
464
|
if (!senBus) {
|
|
378
|
-
const joined = this.client.joinBus(bus);
|
|
465
|
+
const joined = await this.client.joinBus(bus);
|
|
379
466
|
const participantReadyTimeoutMs = this.#participantReadyTimeout(options, timeout);
|
|
380
467
|
await waitForEvent(this.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
|
381
468
|
this.emit('warning', error);
|
|
@@ -429,7 +516,7 @@ export class Sen extends EventEmitter {
|
|
|
429
516
|
|
|
430
517
|
let senBus = this.buses.get(bus);
|
|
431
518
|
if (!senBus) {
|
|
432
|
-
const joined = this.client.joinBus(bus);
|
|
519
|
+
const joined = await this.client.joinBus(bus);
|
|
433
520
|
const participantReadyTimeoutMs = this.#participantReadyTimeout(options, timeout);
|
|
434
521
|
await waitForEvent(this.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
|
435
522
|
this.emit('warning', error);
|
|
@@ -501,9 +588,10 @@ export class Sen extends EventEmitter {
|
|
|
501
588
|
}
|
|
502
589
|
|
|
503
590
|
return [...new Set([
|
|
591
|
+
...this.targets.map(targetSessionName),
|
|
504
592
|
...this.targetsBySession.keys(),
|
|
505
593
|
...this.sessions.keys()
|
|
506
|
-
])].sort();
|
|
594
|
+
].filter(Boolean))].sort();
|
|
507
595
|
}
|
|
508
596
|
|
|
509
597
|
listBuses(options = {}) {
|
|
@@ -519,6 +607,104 @@ export class Sen extends EventEmitter {
|
|
|
519
607
|
.map(busName => options.qualified ? queryBusName(sessionName, busName) : busName);
|
|
520
608
|
}
|
|
521
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Discover visible SEN buses without creating interests or joining buses.
|
|
612
|
+
*
|
|
613
|
+
* @param {object} [options]
|
|
614
|
+
* @param {string} [options.session] Optional session filter.
|
|
615
|
+
* @param {number} [options.busDiscoverySettleMs] Delay after lightweight session connect before reading announced buses.
|
|
616
|
+
* @returns {Promise<Array<{session:string,bus:string,qualified:string}>>}
|
|
617
|
+
*/
|
|
618
|
+
async discoverBuses(options = {}) {
|
|
619
|
+
const config = { ...this.options, ...options };
|
|
620
|
+
const settleMs = Math.max(0, Number(config.busDiscoverySettleMs ?? Math.max(config.discoverySettleMs ?? 100, 1000)) || 0);
|
|
621
|
+
|
|
622
|
+
if (this.client) {
|
|
623
|
+
const sessionName = this.target?.session?.name ?? this.client.processInfo.sessionName;
|
|
624
|
+
return (await waitForSessionBuses(this, settleMs))
|
|
625
|
+
.map(busName => busSummary(sessionName, busName))
|
|
626
|
+
.filter(Boolean)
|
|
627
|
+
.sort((a, b) => a.qualified.localeCompare(b.qualified));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!this.targets.length && !this.sessions.size) {
|
|
631
|
+
const targets = await this.#discoverTargets(config);
|
|
632
|
+
if (!targets.length) {
|
|
633
|
+
throw new Error('no SEN ether processes discovered');
|
|
634
|
+
}
|
|
635
|
+
this.targets = targets;
|
|
636
|
+
for (const target of targets) {
|
|
637
|
+
const sessionName = targetSessionName(target);
|
|
638
|
+
if (sessionName && !this.targetsBySession.has(sessionName)) {
|
|
639
|
+
this.targetsBySession.set(sessionName, target);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const summaries = new Map();
|
|
645
|
+
const addBus = (sessionName, busName) => {
|
|
646
|
+
const summary = busSummary(sessionName, busName);
|
|
647
|
+
if (summary) {
|
|
648
|
+
summaries.set(summary.qualified, summary);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
for (const session of this.sessions.values()) {
|
|
653
|
+
const sessionName = session.target?.session?.name ?? session.client?.processInfo?.sessionName;
|
|
654
|
+
for (const busName of await waitForSessionBuses(session, settleMs)) {
|
|
655
|
+
addBus(sessionName, busName);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const targets = filterTargets(this.targets, config);
|
|
660
|
+
const discoveries = targets.map(async target => {
|
|
661
|
+
const sessionName = targetSessionName(target);
|
|
662
|
+
if (!sessionName) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const session = new Sen({
|
|
667
|
+
...config,
|
|
668
|
+
session: sessionName,
|
|
669
|
+
reconnect: false
|
|
670
|
+
});
|
|
671
|
+
session.on('warning', error => this.emit('warning', error));
|
|
672
|
+
session.on('error', error => this.emit('warning', error));
|
|
673
|
+
try {
|
|
674
|
+
await session.connect({
|
|
675
|
+
...config,
|
|
676
|
+
session: sessionName,
|
|
677
|
+
target,
|
|
678
|
+
reconnect: false
|
|
679
|
+
});
|
|
680
|
+
for (const busName of await waitForSessionBuses(session, settleMs)) {
|
|
681
|
+
addBus(sessionName, busName);
|
|
682
|
+
}
|
|
683
|
+
} finally {
|
|
684
|
+
await session.close().catch(error => this.emit('warning', error));
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const results = await Promise.allSettled(discoveries);
|
|
689
|
+
const failures = [];
|
|
690
|
+
for (const result of results) {
|
|
691
|
+
if (result.status === 'rejected') {
|
|
692
|
+
failures.push(result.reason);
|
|
693
|
+
this.emit('warning', result.reason);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!summaries.size && targets.length && failures.length === targets.length) {
|
|
698
|
+
const sessions = [...new Set(targets.map(targetSessionName).filter(Boolean))].join(', ');
|
|
699
|
+
const error = new Error(`could not read SEN bus announcements from any discovered target${sessions ? ` in sessions: ${sessions}` : ''}`);
|
|
700
|
+
error.code = 'SEN_BUS_DISCOVERY_FAILED';
|
|
701
|
+
error.cause = failures[0];
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return [...summaries.values()].sort((a, b) => a.qualified.localeCompare(b.qualified));
|
|
706
|
+
}
|
|
707
|
+
|
|
522
708
|
objects() {
|
|
523
709
|
if (!this.client) {
|
|
524
710
|
return [...this.sessions.values()].flatMap(session => session.objects());
|
|
@@ -561,13 +747,14 @@ export class Sen extends EventEmitter {
|
|
|
561
747
|
async close() {
|
|
562
748
|
this.manualClose = true;
|
|
563
749
|
for (const session of this.sessions.values()) {
|
|
564
|
-
await session.close();
|
|
750
|
+
await session.close().catch(error => this.emit('warning', error));
|
|
565
751
|
}
|
|
566
752
|
for (const bus of this.buses.values()) {
|
|
567
753
|
bus.close();
|
|
568
754
|
}
|
|
755
|
+
this.#stopPresenceWatchdog();
|
|
569
756
|
await wait(50);
|
|
570
|
-
await this.client?.close();
|
|
757
|
+
await this.client?.close().catch(error => this.emit('warning', error));
|
|
571
758
|
this.client = undefined;
|
|
572
759
|
this.sessions.clear();
|
|
573
760
|
this.buses.clear();
|
|
@@ -675,9 +862,10 @@ export class Sen extends EventEmitter {
|
|
|
675
862
|
this.emit('error', error);
|
|
676
863
|
});
|
|
677
864
|
client.on('close', hadError => {
|
|
865
|
+
this.#stopPresenceWatchdog();
|
|
678
866
|
this.emit('close', hadError);
|
|
679
867
|
if (!this.manualClose && this.options.reconnect !== false) {
|
|
680
|
-
this.#reconnect().catch(error => this.emit('
|
|
868
|
+
this.#reconnect().catch(error => this.emit('warning', error));
|
|
681
869
|
}
|
|
682
870
|
});
|
|
683
871
|
}
|
|
@@ -688,12 +876,15 @@ export class Sen extends EventEmitter {
|
|
|
688
876
|
}
|
|
689
877
|
|
|
690
878
|
this.reconnecting = true;
|
|
879
|
+
this.#stopPresenceWatchdog();
|
|
691
880
|
await this.client?.close().catch(error => this.emit('warning', error));
|
|
692
881
|
this.emit('reconnecting');
|
|
693
|
-
const
|
|
882
|
+
const configuredMaxAttempts = this.connectOptions.maxReconnectAttempts ?? this.options.maxReconnectAttempts ?? 0;
|
|
883
|
+
const maxAttempts = Number(configuredMaxAttempts);
|
|
884
|
+
const unlimited = !Number.isFinite(maxAttempts) || maxAttempts <= 0;
|
|
694
885
|
const delayMs = this.connectOptions.reconnectDelayMs ?? this.options.reconnectDelayMs ?? 500;
|
|
695
886
|
|
|
696
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
887
|
+
for (let attempt = 1; unlimited || attempt <= maxAttempts; attempt += 1) {
|
|
697
888
|
let client;
|
|
698
889
|
try {
|
|
699
890
|
await wait(delayMs);
|
|
@@ -727,6 +918,7 @@ export class Sen extends EventEmitter {
|
|
|
727
918
|
|
|
728
919
|
await client.connect(target);
|
|
729
920
|
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
921
|
+
this.#startPresenceWatchdog(target, config);
|
|
730
922
|
|
|
731
923
|
for (const bus of this.buses.values()) {
|
|
732
924
|
await bus.rejoin(config.timeout ?? 3000);
|
|
@@ -737,6 +929,10 @@ export class Sen extends EventEmitter {
|
|
|
737
929
|
return;
|
|
738
930
|
} catch (error) {
|
|
739
931
|
await client?.close().catch(closeError => this.emit('warning', closeError));
|
|
932
|
+
if (this.client === client) {
|
|
933
|
+
this.client = undefined;
|
|
934
|
+
this.target = undefined;
|
|
935
|
+
}
|
|
740
936
|
if (this.manualClose) {
|
|
741
937
|
this.reconnecting = false;
|
|
742
938
|
return;
|
|
@@ -754,6 +950,74 @@ export class Sen extends EventEmitter {
|
|
|
754
950
|
throw new Error(`failed to reconnect SEN ether after ${maxAttempts} attempt(s)`);
|
|
755
951
|
}
|
|
756
952
|
|
|
953
|
+
#startPresenceWatchdog(target, config) {
|
|
954
|
+
this.#stopPresenceWatchdog();
|
|
955
|
+
const key = target?.key;
|
|
956
|
+
if (!key) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const timeoutMs = Number(config.presenceTimeoutMs ?? this.options.presenceTimeoutMs ?? 0);
|
|
961
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const intervalMs = Math.max(
|
|
966
|
+
250,
|
|
967
|
+
Number(config.presenceCheckIntervalMs ?? this.options.presenceCheckIntervalMs ?? 1000) || 1000
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const scanner = config.tcpHub
|
|
971
|
+
? new TcpDiscoveryHubScanner(parseHostPort(config.tcpHub))
|
|
972
|
+
: new EtherDiscoveryScanner(config);
|
|
973
|
+
|
|
974
|
+
this.presenceScanner = scanner;
|
|
975
|
+
this.presenceLastSeen = Date.now();
|
|
976
|
+
|
|
977
|
+
scanner.on('beam', process => {
|
|
978
|
+
if (process.key === key) {
|
|
979
|
+
this.presenceLastSeen = Date.now();
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
scanner.on('error', error => this.emit('warning', error));
|
|
983
|
+
scanner.on('close', hadError => {
|
|
984
|
+
if (!this.manualClose && !this.reconnecting) {
|
|
985
|
+
this.emit('warning', new Error(`SEN ether discovery watchdog closed${hadError ? ' with error' : ''}`));
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
scanner.start().catch(error => this.emit('warning', error));
|
|
989
|
+
|
|
990
|
+
this.presenceTimer = setInterval(() => {
|
|
991
|
+
if (this.manualClose || this.reconnecting || !this.client) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const elapsedMs = Date.now() - this.presenceLastSeen;
|
|
995
|
+
if (elapsedMs <= timeoutMs) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const error = new Error(`SEN ether presence timeout after ${elapsedMs}ms without beam from ${key}`);
|
|
999
|
+
error.code = 'SEN_PRESENCE_TIMEOUT';
|
|
1000
|
+
this.emit('warning', error);
|
|
1001
|
+
this.client.socket?.destroy(error);
|
|
1002
|
+
}, intervalMs);
|
|
1003
|
+
this.presenceTimer.unref?.();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
#stopPresenceWatchdog() {
|
|
1007
|
+
if (this.presenceTimer) {
|
|
1008
|
+
clearInterval(this.presenceTimer);
|
|
1009
|
+
this.presenceTimer = undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const scanner = this.presenceScanner;
|
|
1013
|
+
this.presenceScanner = undefined;
|
|
1014
|
+
this.presenceLastSeen = 0;
|
|
1015
|
+
if (scanner) {
|
|
1016
|
+
scanner.removeAllListeners();
|
|
1017
|
+
scanner.stop().catch(error => this.emit('warning', error));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
757
1021
|
#busForEvent(event) {
|
|
758
1022
|
if (event.bus?.busName) {
|
|
759
1023
|
return this.buses.get(event.bus.busName);
|
|
@@ -814,18 +1078,29 @@ export class SenBus extends EventEmitter {
|
|
|
814
1078
|
const interestId = typeof id === 'object' ? id.id : id;
|
|
815
1079
|
this.sen.client.stopInterest(this.name, interestId);
|
|
816
1080
|
const interest = this.interests.get(interestId);
|
|
1081
|
+
this.#detachInterestObjects(interestId, interest);
|
|
817
1082
|
this.interests.delete(interestId);
|
|
818
1083
|
interest?.closeLocal();
|
|
819
1084
|
interest?.emit('close');
|
|
820
1085
|
}
|
|
821
1086
|
|
|
822
1087
|
close() {
|
|
823
|
-
for (const interest of this.interests.values()) {
|
|
824
|
-
|
|
825
|
-
|
|
1088
|
+
for (const interest of [...this.interests.values()]) {
|
|
1089
|
+
try {
|
|
1090
|
+
this.stopInterest(interest.id);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
this.#detachInterestObjects(interest.id, interest);
|
|
1093
|
+
this.interests.delete(interest.id);
|
|
1094
|
+
interest.closeLocal();
|
|
1095
|
+
interest.emit('close');
|
|
1096
|
+
this.sen.emit('warning', error);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
this.sen.client.leaveBus(this.name);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
this.sen.emit('warning', error);
|
|
826
1103
|
}
|
|
827
|
-
this.interests.clear();
|
|
828
|
-
this.sen.client.leaveBus(this.name);
|
|
829
1104
|
}
|
|
830
1105
|
|
|
831
1106
|
prepareReconnect() {
|
|
@@ -854,7 +1129,7 @@ export class SenBus extends EventEmitter {
|
|
|
854
1129
|
await this.sen.waitForRemoteBus(this.name, busReadyTimeoutMs).catch(error => {
|
|
855
1130
|
this.sen.emit('warning', error);
|
|
856
1131
|
});
|
|
857
|
-
const joined = this.sen.client.joinBus(this.name);
|
|
1132
|
+
const joined = await this.sen.client.joinBus(this.name);
|
|
858
1133
|
this.id = joined.busId;
|
|
859
1134
|
const participantReadyTimeoutMs = Math.min(timeoutMs, this.sen.options.participantReadyTimeoutMs ?? 1000);
|
|
860
1135
|
await waitForEvent(this.sen.client, 'busParticipantReady', participantReadyTimeoutMs).catch(error => {
|
|
@@ -884,24 +1159,36 @@ export class SenBus extends EventEmitter {
|
|
|
884
1159
|
const newTypeHashes = new Set();
|
|
885
1160
|
for (const discovery of event.discoveries ?? []) {
|
|
886
1161
|
for (const info of discovery.objects ?? []) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1162
|
+
let object = this.objectsById.get(info.id);
|
|
1163
|
+
const isNewObject = !object;
|
|
1164
|
+
if (!object) {
|
|
1165
|
+
object = new SenRemoteObject(this, {
|
|
1166
|
+
...info,
|
|
1167
|
+
ownerId: event.ownerId,
|
|
1168
|
+
interestId: discovery.interestId
|
|
1169
|
+
});
|
|
1170
|
+
this.objectsById.set(object.id, object);
|
|
1171
|
+
} else {
|
|
1172
|
+
object.attachInterest(discovery.interestId);
|
|
1173
|
+
object.updateDiscoveryInfo({
|
|
1174
|
+
...info,
|
|
1175
|
+
ownerId: event.ownerId
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
893
1178
|
const interest = this.interests.get(discovery.interestId);
|
|
894
1179
|
interest?.objectsById.set(object.id, object);
|
|
895
1180
|
if (info.state?.length) {
|
|
896
|
-
object.applyState(info.state, 'state', info.time);
|
|
1181
|
+
object.applyState(info.state, 'state', info.time, { interestId: discovery.interestId });
|
|
897
1182
|
}
|
|
898
1183
|
if (!this.requestedTypeHashes.has(info.typeHash)) {
|
|
899
1184
|
this.requestedTypeHashes.add(info.typeHash);
|
|
900
1185
|
newTypeHashes.add(info.typeHash);
|
|
901
1186
|
}
|
|
902
1187
|
interest?.emit('object', object);
|
|
903
|
-
|
|
904
|
-
|
|
1188
|
+
if (isNewObject) {
|
|
1189
|
+
this.emit('object', object);
|
|
1190
|
+
this.sen.emit('object', object);
|
|
1191
|
+
}
|
|
905
1192
|
}
|
|
906
1193
|
}
|
|
907
1194
|
if (newTypeHashes.size) {
|
|
@@ -914,20 +1201,47 @@ export class SenBus extends EventEmitter {
|
|
|
914
1201
|
for (const removal of event.removals ?? []) {
|
|
915
1202
|
for (const id of removal.ids ?? []) {
|
|
916
1203
|
const object = this.objectsById.get(id);
|
|
917
|
-
this.objectsById.delete(id);
|
|
918
|
-
this.stateRequestedObjectIds.delete(id);
|
|
919
1204
|
const interest = this.interests.get(removal.interestId);
|
|
920
1205
|
interest?.objectsById.delete(id);
|
|
1206
|
+
this.stateRequestedObjectIds.delete(stateRequestKey(removal.interestId, id));
|
|
921
1207
|
if (object) {
|
|
922
|
-
object.
|
|
1208
|
+
object.detachInterest(removal.interestId);
|
|
1209
|
+
object.emit('remove', { interestId: removal.interestId });
|
|
923
1210
|
interest?.emit('remove', object);
|
|
924
|
-
|
|
925
|
-
|
|
1211
|
+
if (object.interestIds.size === 0) {
|
|
1212
|
+
this.objectsById.delete(id);
|
|
1213
|
+
this.requestedTypeHashes.delete(object.typeHash);
|
|
1214
|
+
this.emit('remove', object);
|
|
1215
|
+
this.sen.emit('remove', object);
|
|
1216
|
+
}
|
|
926
1217
|
}
|
|
927
1218
|
}
|
|
928
1219
|
}
|
|
929
1220
|
}
|
|
930
1221
|
|
|
1222
|
+
#detachInterestObjects(interestId, interest) {
|
|
1223
|
+
const normalizedInterestId = interestId >>> 0;
|
|
1224
|
+
const keyPrefix = `${normalizedInterestId}:`;
|
|
1225
|
+
for (const key of [...this.stateRequestedObjectIds]) {
|
|
1226
|
+
if (key.startsWith(keyPrefix)) {
|
|
1227
|
+
this.stateRequestedObjectIds.delete(key);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (!interest) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
for (const object of interest.objectsById.values()) {
|
|
1236
|
+
object.detachInterest(normalizedInterestId);
|
|
1237
|
+
if (object.interestIds.size === 0) {
|
|
1238
|
+
this.objectsById.delete(object.id);
|
|
1239
|
+
this.requestedTypeHashes.delete(object.typeHash);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
interest.objectsById.clear();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
931
1245
|
handleTypesInfoResponse(event) {
|
|
932
1246
|
const dependentTypeHashes = new Set();
|
|
933
1247
|
for (const type of event.types ?? []) {
|
|
@@ -962,7 +1276,7 @@ export class SenBus extends EventEmitter {
|
|
|
962
1276
|
if (!object) {
|
|
963
1277
|
continue;
|
|
964
1278
|
}
|
|
965
|
-
object.applyState(state.state, 'state', state.timestamp);
|
|
1279
|
+
object.applyState(state.state, 'state', state.timestamp, { interestId: response.interestId });
|
|
966
1280
|
}
|
|
967
1281
|
}
|
|
968
1282
|
}
|
|
@@ -1042,14 +1356,20 @@ export class SenBus extends EventEmitter {
|
|
|
1042
1356
|
|
|
1043
1357
|
#requestReadyObjectStates() {
|
|
1044
1358
|
const requestsByInterest = new Map();
|
|
1045
|
-
for (const
|
|
1046
|
-
|
|
1047
|
-
|
|
1359
|
+
for (const interest of this.interests.values()) {
|
|
1360
|
+
for (const object of interest.objectsById.values()) {
|
|
1361
|
+
if (!object.spec) {
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const key = stateRequestKey(interest.id, object.id);
|
|
1365
|
+
if (this.stateRequestedObjectIds.has(key)) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
this.stateRequestedObjectIds.add(key);
|
|
1369
|
+
const ids = requestsByInterest.get(interest.id) ?? [];
|
|
1370
|
+
ids.push(object.id);
|
|
1371
|
+
requestsByInterest.set(interest.id, ids);
|
|
1048
1372
|
}
|
|
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
1373
|
}
|
|
1054
1374
|
|
|
1055
1375
|
if (requestsByInterest.size) {
|
|
@@ -1066,7 +1386,8 @@ export class SenBus extends EventEmitter {
|
|
|
1066
1386
|
object.applyState(
|
|
1067
1387
|
object.pendingState.buffer,
|
|
1068
1388
|
object.pendingState.source,
|
|
1069
|
-
object.pendingState.timestampNs
|
|
1389
|
+
object.pendingState.timestampNs,
|
|
1390
|
+
{ interestId: object.pendingState.interestId }
|
|
1070
1391
|
);
|
|
1071
1392
|
}
|
|
1072
1393
|
}
|
|
@@ -1164,6 +1485,10 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1164
1485
|
this.typeHash = info.typeHash;
|
|
1165
1486
|
this.ownerId = info.ownerId;
|
|
1166
1487
|
this.interestId = info.interestId;
|
|
1488
|
+
this.interestIds = new Set();
|
|
1489
|
+
if (info.interestId !== undefined) {
|
|
1490
|
+
this.interestIds.add(info.interestId);
|
|
1491
|
+
}
|
|
1167
1492
|
this.snapshot = {};
|
|
1168
1493
|
this.spec = undefined;
|
|
1169
1494
|
this.pendingState = undefined;
|
|
@@ -1186,6 +1511,29 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1186
1511
|
return this.name === selector || this.className === selector || String(this.id) === String(selector);
|
|
1187
1512
|
}
|
|
1188
1513
|
|
|
1514
|
+
attachInterest(interestId) {
|
|
1515
|
+
if (interestId !== undefined) {
|
|
1516
|
+
this.interestIds.add(interestId);
|
|
1517
|
+
this.interestId = interestId;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
detachInterest(interestId) {
|
|
1522
|
+
if (interestId !== undefined) {
|
|
1523
|
+
this.interestIds.delete(interestId);
|
|
1524
|
+
if (this.interestId === interestId) {
|
|
1525
|
+
this.interestId = this.interestIds.values().next().value;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
updateDiscoveryInfo(info) {
|
|
1531
|
+
this.name = info.name ?? this.name;
|
|
1532
|
+
this.className = info.className ?? this.className;
|
|
1533
|
+
this.typeHash = info.typeHash ?? this.typeHash;
|
|
1534
|
+
this.ownerId = info.ownerId ?? this.ownerId;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1189
1537
|
property(name) {
|
|
1190
1538
|
return findByName(collectClassMembers(this.spec, this.bus.typeRegistry, 'properties'), name);
|
|
1191
1539
|
}
|
|
@@ -1259,17 +1607,18 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1259
1607
|
return await this.bus.callObjectMethod(this, method, args, options);
|
|
1260
1608
|
}
|
|
1261
1609
|
|
|
1262
|
-
applyState(buffer, source, timestamp) {
|
|
1610
|
+
applyState(buffer, source, timestamp, options = {}) {
|
|
1263
1611
|
const timestampNs = normalizeTimestampNs(timestamp);
|
|
1264
1612
|
this.#rememberObjectTimestamp(source, timestampNs);
|
|
1265
1613
|
|
|
1266
1614
|
if (!this.spec) {
|
|
1267
|
-
this.pendingState = { buffer, source, timestampNs };
|
|
1615
|
+
this.pendingState = { buffer, source, timestampNs, interestId: options.interestId };
|
|
1268
1616
|
return;
|
|
1269
1617
|
}
|
|
1270
1618
|
|
|
1271
|
-
const
|
|
1272
|
-
const
|
|
1619
|
+
const interests = this.#targetInterests(options.interestId);
|
|
1620
|
+
const decodeInterest = options.interestId !== undefined || interests.length === 1 ? interests[0] : undefined;
|
|
1621
|
+
const values = decodePropertyValues(buffer, this.spec, this.bus.typeRegistry, decodeInterest?.decodeOptions());
|
|
1273
1622
|
let complete = true;
|
|
1274
1623
|
for (const value of values) {
|
|
1275
1624
|
if (!value.decoded) {
|
|
@@ -1292,9 +1641,10 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1292
1641
|
previous,
|
|
1293
1642
|
property: value.property
|
|
1294
1643
|
};
|
|
1295
|
-
|
|
1644
|
+
for (const interest of interests) {
|
|
1296
1645
|
interest.publishChange(change);
|
|
1297
|
-
}
|
|
1646
|
+
}
|
|
1647
|
+
if (!interests.length) {
|
|
1298
1648
|
this.emit('change', change);
|
|
1299
1649
|
this.emit(`change:${value.name}`, change);
|
|
1300
1650
|
this.bus.emit('change', change);
|
|
@@ -1302,7 +1652,22 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1302
1652
|
}
|
|
1303
1653
|
}
|
|
1304
1654
|
|
|
1305
|
-
this.pendingState = complete ? undefined : { buffer, source, timestampNs };
|
|
1655
|
+
this.pendingState = complete ? undefined : { buffer, source, timestampNs, interestId: options.interestId };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
#targetInterests(interestId) {
|
|
1659
|
+
if (interestId !== undefined) {
|
|
1660
|
+
const interest = this.bus.interests.get(interestId);
|
|
1661
|
+
return interest ? [interest] : [];
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const interests = [];
|
|
1665
|
+
for (const interest of this.bus.interests.values()) {
|
|
1666
|
+
if (interest.objectsById.has(this.id)) {
|
|
1667
|
+
interests.push(interest);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return interests;
|
|
1306
1671
|
}
|
|
1307
1672
|
|
|
1308
1673
|
#rememberObjectTimestamp(source, timestampNs) {
|
package/lib/values.js
CHANGED
|
@@ -70,9 +70,7 @@ function collectClassProperties(spec, typeRegistry, seen = new Set()) {
|
|
|
70
70
|
|
|
71
71
|
function decodeEnum(reader, spec) {
|
|
72
72
|
const storage = spec.data.value.storageType;
|
|
73
|
-
|
|
74
|
-
const item = spec.data.value.enums.find(candidate => candidate.key === key);
|
|
75
|
-
return item?.name ?? key;
|
|
73
|
+
return decodeValueFromReader(reader, storage);
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
function decodeStruct(reader, spec, typeRegistry) {
|