sen-ether-client 0.1.7 → 0.2.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 +69 -6
- package/README.md +54 -2
- package/index.js +26 -0
- package/lib/bus.js +261 -1
- package/lib/client.js +1016 -97
- package/lib/sen.js +164 -46
- package/lib/values.js +14 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
encodeConfirmedBusFrame,
|
|
12
12
|
encodeRuntimeMethodCall
|
|
13
13
|
} from './bus.js';
|
|
14
|
+
import { encodePropertyUpdateBuffer } from './values.js';
|
|
14
15
|
import {
|
|
15
16
|
decodeEtherControlMessage,
|
|
16
17
|
decodeProcessTcpHeader,
|
|
18
|
+
decodeSessionPresenceBeam,
|
|
17
19
|
encodeEtherControlMessage,
|
|
18
20
|
encodeProcessTcpFrame,
|
|
21
|
+
encodeSessionPresenceBeam,
|
|
19
22
|
ETHER_PROTOCOL_VERSION,
|
|
20
23
|
KERNEL_PROTOCOL_VERSION,
|
|
21
24
|
PROCESS_MESSAGE_CATEGORY
|
|
@@ -24,8 +27,11 @@ import { crc32 } from './crc32.js';
|
|
|
24
27
|
|
|
25
28
|
const LINUX_OS_KIND = 1;
|
|
26
29
|
const X64_CPU_ARCH = 1;
|
|
30
|
+
const DEFAULT_DISCOVERY_GROUP = '239.255.0.44';
|
|
27
31
|
const DEFAULT_DISCOVERY_PORT = 60543;
|
|
28
32
|
const DEFAULT_BUS_MULTICAST_PORT = 50985;
|
|
33
|
+
const TCP_DISCOVERY_BEAM_SIZE = 508;
|
|
34
|
+
const DEFAULT_BEAM_PERIOD_MS = 1000;
|
|
29
35
|
const BUS_HASH_SEED = 15071983;
|
|
30
36
|
const FNV1A_OFFSET_BASIS = 0x811c9dc5;
|
|
31
37
|
const FNV1A_PRIME = 0x01000193;
|
|
@@ -169,6 +175,183 @@ function computeBusMulticastGroup(sessionId, busId, discoveryPort, ranges) {
|
|
|
169
175
|
return bytes.map((byte, index) => Math.min(Math.max(byte, ranges[index].min), ranges[index].max)).join('.');
|
|
170
176
|
}
|
|
171
177
|
|
|
178
|
+
function classSpecData(spec) {
|
|
179
|
+
return spec?.data?.type === 'ClassTypeSpec' ? spec.data.value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function localTypeSpec(typeRegistry, typeName) {
|
|
183
|
+
return typeRegistry?.get?.(typeName) ?? typeRegistry?.[typeName];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function collectClassProperties(spec, typeRegistry, seen = new Set()) {
|
|
187
|
+
const data = classSpecData(spec);
|
|
188
|
+
const key = spec?.qualifiedName ?? spec?.name;
|
|
189
|
+
if (!data || seen.has(key)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
seen.add(key);
|
|
193
|
+
return [
|
|
194
|
+
...(data.parents ?? []).flatMap(parent => collectClassProperties(localTypeSpec(typeRegistry, parent), typeRegistry, seen)),
|
|
195
|
+
...(data.properties ?? [])
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function inferValueType(value) {
|
|
200
|
+
if (typeof value === 'boolean') return 'bool';
|
|
201
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'i64' : 'f64';
|
|
202
|
+
if (typeof value === 'bigint') return 'i64';
|
|
203
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer) return 'Buffer';
|
|
204
|
+
if (typeof value === 'string' || value === null || value === undefined) return 'string';
|
|
205
|
+
throw new TypeError('cannot infer SEN type for structured value; pass an explicit spec and dependent types');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ensureClassSpec(className, state = {}, spec) {
|
|
209
|
+
if (spec) return spec;
|
|
210
|
+
const properties = Object.entries(state || {}).map(([name, value]) => ({
|
|
211
|
+
name,
|
|
212
|
+
description: '',
|
|
213
|
+
category: 'dynamicRO',
|
|
214
|
+
type: inferValueType(value),
|
|
215
|
+
transportMode: 'confirmed',
|
|
216
|
+
tags: [],
|
|
217
|
+
checkedSet: false
|
|
218
|
+
}));
|
|
219
|
+
return {
|
|
220
|
+
name: String(className || '').split('.').pop() || String(className || ''),
|
|
221
|
+
qualifiedName: className,
|
|
222
|
+
description: '',
|
|
223
|
+
data: {
|
|
224
|
+
type: 'ClassTypeSpec',
|
|
225
|
+
value: {
|
|
226
|
+
properties,
|
|
227
|
+
methods: [],
|
|
228
|
+
events: [],
|
|
229
|
+
constructor: { name: '', description: '', args: [], returnType: '' },
|
|
230
|
+
parents: [],
|
|
231
|
+
isInterface: false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeTypeDefinitions(typeDefinitions = []) {
|
|
238
|
+
const values = typeDefinitions instanceof Map
|
|
239
|
+
? [...typeDefinitions.values()]
|
|
240
|
+
: Array.isArray(typeDefinitions)
|
|
241
|
+
? typeDefinitions
|
|
242
|
+
: Object.values(typeDefinitions || {});
|
|
243
|
+
return values.filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function processKeyFromInfo(info = {}) {
|
|
247
|
+
return `${info.hostId}:${info.processId}:${info.sessionId}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isSameProcessInfo(a = {}, b = {}) {
|
|
251
|
+
return (
|
|
252
|
+
(a.hostId >>> 0) === (b.hostId >>> 0) &&
|
|
253
|
+
(a.processId >>> 0) === (b.processId >>> 0) &&
|
|
254
|
+
(a.sessionId >>> 0) === (b.sessionId >>> 0)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function firstAdvertisableAddress(interfaceAddress) {
|
|
259
|
+
if (interfaceAddress && interfaceAddress !== '0.0.0.0') {
|
|
260
|
+
return interfaceAddress;
|
|
261
|
+
}
|
|
262
|
+
for (const candidates of Object.values(os.networkInterfaces())) {
|
|
263
|
+
for (const item of candidates ?? []) {
|
|
264
|
+
if ((item.family === 'IPv4' || item.family === 4) && !item.internal && item.address) {
|
|
265
|
+
return item.address;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return '127.0.0.1';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseHostPort(value, fallbackPort) {
|
|
273
|
+
if (!value) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (typeof value === 'object') {
|
|
277
|
+
return { host: value.host ?? '127.0.0.1', port: Number(value.port ?? fallbackPort) };
|
|
278
|
+
}
|
|
279
|
+
const text = String(value).trim();
|
|
280
|
+
const idx = text.lastIndexOf(':');
|
|
281
|
+
if (idx <= 0) {
|
|
282
|
+
return { host: text || '127.0.0.1', port: Number(fallbackPort) };
|
|
283
|
+
}
|
|
284
|
+
return { host: text.slice(0, idx), port: Number(text.slice(idx + 1)) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function padDiscoveryBeam(buffer) {
|
|
288
|
+
if (buffer.length > TCP_DISCOVERY_BEAM_SIZE) {
|
|
289
|
+
throw new Error(`SEN discovery beam is too large: ${buffer.length} > ${TCP_DISCOVERY_BEAM_SIZE}`);
|
|
290
|
+
}
|
|
291
|
+
if (buffer.length === TCP_DISCOVERY_BEAM_SIZE) {
|
|
292
|
+
return buffer;
|
|
293
|
+
}
|
|
294
|
+
const padded = Buffer.alloc(TCP_DISCOVERY_BEAM_SIZE);
|
|
295
|
+
Buffer.from(buffer).copy(padded);
|
|
296
|
+
return padded;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function withCloseTimeout(register, timeoutMs = 1000) {
|
|
300
|
+
return new Promise(resolve => {
|
|
301
|
+
let done = false;
|
|
302
|
+
const finish = () => {
|
|
303
|
+
if (done) return;
|
|
304
|
+
done = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
resolve();
|
|
307
|
+
};
|
|
308
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
309
|
+
timer.unref?.();
|
|
310
|
+
try {
|
|
311
|
+
register(finish);
|
|
312
|
+
} catch {
|
|
313
|
+
finish();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildLocalObject(input, typeRegistry) {
|
|
319
|
+
const className = String(input.className ?? input.classname ?? input.type ?? '').trim();
|
|
320
|
+
if (!className) {
|
|
321
|
+
throw new TypeError('SEN published object requires className');
|
|
322
|
+
}
|
|
323
|
+
const name = String(input.name ?? '').trim();
|
|
324
|
+
if (!name) {
|
|
325
|
+
throw new TypeError('SEN published object requires name');
|
|
326
|
+
}
|
|
327
|
+
const id = input.id ?? crc32(name);
|
|
328
|
+
const typeHash = input.typeHash ?? crc32(className);
|
|
329
|
+
const state = input.state ?? input.snapshot ?? input.properties ?? {};
|
|
330
|
+
const spec = ensureClassSpec(className, state, input.spec);
|
|
331
|
+
const registry = new Map(typeRegistry);
|
|
332
|
+
registry.set(spec.qualifiedName, spec);
|
|
333
|
+
const properties = collectClassProperties(spec, registry);
|
|
334
|
+
const stateBuffer = input.stateBuffer
|
|
335
|
+
? Buffer.from(input.stateBuffer)
|
|
336
|
+
: encodePropertyUpdateBuffer(
|
|
337
|
+
properties
|
|
338
|
+
.filter(property => Object.prototype.hasOwnProperty.call(state, property.name))
|
|
339
|
+
.map(property => ({ name: property.name, type: property.type, value: state[property.name] })),
|
|
340
|
+
registry
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
id: id >>> 0,
|
|
345
|
+
name,
|
|
346
|
+
className,
|
|
347
|
+
typeHash: typeHash >>> 0,
|
|
348
|
+
spec,
|
|
349
|
+
state,
|
|
350
|
+
stateBuffer,
|
|
351
|
+
timestamp: input.timestamp ?? input.time ?? BigInt(Date.now()) * 1_000_000n
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
172
355
|
/**
|
|
173
356
|
* Create a ProcessInfo compatible with sen::kernel::getOwnProcessInfo.
|
|
174
357
|
*
|
|
@@ -251,7 +434,15 @@ export class EtherClient extends EventEmitter {
|
|
|
251
434
|
socketKeepAlive: true,
|
|
252
435
|
socketKeepAliveInitialDelayMs: 1000,
|
|
253
436
|
socketIdleTimeoutMs: 0,
|
|
437
|
+
group: DEFAULT_DISCOVERY_GROUP,
|
|
438
|
+
bindAddress: undefined,
|
|
254
439
|
discoveryPort: discoveryPortFromEnv() ?? DEFAULT_DISCOVERY_PORT,
|
|
440
|
+
tcpHub: undefined,
|
|
441
|
+
listen: true,
|
|
442
|
+
listenHost: '0.0.0.0',
|
|
443
|
+
listenPort: 0,
|
|
444
|
+
advertisedHost: undefined,
|
|
445
|
+
beamPeriodMs: DEFAULT_BEAM_PERIOD_MS,
|
|
255
446
|
busMulticastPort: DEFAULT_BUS_MULTICAST_PORT,
|
|
256
447
|
busMulticastRange: DEFAULT_MULTICAST_RANGE,
|
|
257
448
|
...options
|
|
@@ -264,107 +455,449 @@ export class EtherClient extends EventEmitter {
|
|
|
264
455
|
this.busMulticastRange = normalizeMulticastRange(this.options.busMulticastRange);
|
|
265
456
|
this.socket = undefined;
|
|
266
457
|
this.udpSocket = undefined;
|
|
458
|
+
this.server = undefined;
|
|
459
|
+
this.discoverySocket = undefined;
|
|
460
|
+
this.discoveryReceiveBuffer = Buffer.alloc(0);
|
|
461
|
+
this.discoveryTimer = undefined;
|
|
462
|
+
this.multicastDiscoverySocket = undefined;
|
|
463
|
+
this.multicastDiscoveryTimer = undefined;
|
|
464
|
+
this.connections = new Map();
|
|
465
|
+
this.connectionsByProcessKey = new Map();
|
|
466
|
+
this.nextConnectionId = 1;
|
|
467
|
+
this.listenEndpoint = undefined;
|
|
267
468
|
this.receiveBuffer = Buffer.alloc(0);
|
|
268
469
|
this.remoteProcessInfo = undefined;
|
|
269
470
|
this.ready = false;
|
|
270
471
|
this.buses = new Map();
|
|
472
|
+
this.remoteParticipantsByBusId = new Map();
|
|
473
|
+
this.nextInterestId = randomUInt32();
|
|
271
474
|
}
|
|
272
475
|
|
|
273
476
|
/**
|
|
274
|
-
*
|
|
477
|
+
* Start this JS process as an active Ether node.
|
|
275
478
|
*
|
|
276
|
-
*
|
|
479
|
+
* It opens a TCP listener for process-to-process traffic and, when `tcpHub`
|
|
480
|
+
* is configured, beams its presence to the hub while connecting to compatible
|
|
481
|
+
* remote processes announced by the hub.
|
|
277
482
|
*/
|
|
278
|
-
async
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
483
|
+
async start(options = {}) {
|
|
484
|
+
const config = { ...this.options, ...options };
|
|
485
|
+
this.options = config;
|
|
486
|
+
this.interfaceAddress = resolveInterfaceAddress(this.options.interfaceAddress);
|
|
487
|
+
if (config.listen !== false && !this.server) {
|
|
488
|
+
await this.#startServer(config);
|
|
489
|
+
}
|
|
490
|
+
if (config.tcpHub && !this.discoverySocket) {
|
|
491
|
+
await this.#startTcpDiscovery(config);
|
|
492
|
+
}
|
|
493
|
+
if (!config.tcpHub && config.multicastDiscovery !== false && !this.multicastDiscoverySocket) {
|
|
494
|
+
await this.#startMulticastDiscovery(config);
|
|
282
495
|
}
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async #startServer(config) {
|
|
500
|
+
const server = net.createServer(socket => {
|
|
501
|
+
const connection = this.#registerSocket(socket, { incoming: true });
|
|
502
|
+
this.#configureTcpSocket(socket);
|
|
503
|
+
this.#sendHello(connection);
|
|
504
|
+
});
|
|
505
|
+
this.server = server;
|
|
506
|
+
server.on('error', error => this.emit('error', error));
|
|
283
507
|
|
|
284
|
-
this.udpSocket = dgram.createSocket('udp4');
|
|
285
|
-
this.udpSocket.on('message', message => this.#onBusFrame(message));
|
|
286
|
-
this.udpSocket.on('error', error => this.emit('error', error));
|
|
287
508
|
await new Promise((resolve, reject) => {
|
|
288
509
|
const onError = error => {
|
|
289
|
-
|
|
510
|
+
server.off('listening', onListening);
|
|
290
511
|
reject(error);
|
|
291
512
|
};
|
|
292
513
|
const onListening = () => {
|
|
293
|
-
|
|
514
|
+
server.off('error', onError);
|
|
515
|
+
const address = server.address();
|
|
516
|
+
const port = typeof address === 'object' && address ? address.port : config.listenPort;
|
|
517
|
+
const listenHost = config.listenHost ?? '0.0.0.0';
|
|
518
|
+
this.listenEndpoint = {
|
|
519
|
+
host: config.advertisedHost ?? (listenHost === '0.0.0.0' || listenHost === '::'
|
|
520
|
+
? firstAdvertisableAddress(this.interfaceAddress)
|
|
521
|
+
: listenHost),
|
|
522
|
+
port
|
|
523
|
+
};
|
|
524
|
+
this.emit('listening', this.listenEndpoint);
|
|
294
525
|
resolve();
|
|
295
526
|
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
527
|
+
server.once('error', onError);
|
|
528
|
+
server.once('listening', onListening);
|
|
529
|
+
server.listen(config.listenPort ?? 0, config.listenHost ?? '0.0.0.0');
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async #startTcpDiscovery(config) {
|
|
534
|
+
const hub = parseHostPort(config.tcpHub, 64222);
|
|
535
|
+
if (!hub?.host || !Number.isInteger(hub.port) || hub.port <= 0) {
|
|
536
|
+
throw new Error(`invalid SEN TCP discovery hub: ${config.tcpHub}`);
|
|
537
|
+
}
|
|
538
|
+
const socket = net.createConnection({ host: hub.host, port: hub.port });
|
|
539
|
+
this.discoverySocket = socket;
|
|
540
|
+
socket.on('data', chunk => this.#onDiscoveryData(chunk));
|
|
541
|
+
socket.on('close', hadError => {
|
|
542
|
+
if (this.discoverySocket === socket) {
|
|
543
|
+
this.discoverySocket = undefined;
|
|
544
|
+
}
|
|
545
|
+
if (this.discoveryTimer) {
|
|
546
|
+
clearInterval(this.discoveryTimer);
|
|
547
|
+
this.discoveryTimer = undefined;
|
|
548
|
+
}
|
|
549
|
+
this.emit('discoveryClose', hadError);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await new Promise((resolve, reject) => {
|
|
553
|
+
const onError = error => {
|
|
554
|
+
socket.off('connect', onConnect);
|
|
555
|
+
reject(error);
|
|
556
|
+
};
|
|
557
|
+
const onConnect = () => {
|
|
558
|
+
socket.off('error', onError);
|
|
559
|
+
socket.on('error', error => this.emit('error', error));
|
|
560
|
+
this.#sendDiscoveryBeam();
|
|
561
|
+
const period = Math.max(100, Number(config.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS));
|
|
562
|
+
this.discoveryTimer = setInterval(() => this.#sendDiscoveryBeam(), period);
|
|
563
|
+
this.discoveryTimer.unref?.();
|
|
564
|
+
this.emit('discoveryConnect', hub);
|
|
565
|
+
resolve();
|
|
566
|
+
};
|
|
567
|
+
socket.once('error', onError);
|
|
568
|
+
socket.once('connect', onConnect);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async #startMulticastDiscovery(config) {
|
|
573
|
+
if (!this.listenEndpoint) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const group = config.group ?? DEFAULT_DISCOVERY_GROUP;
|
|
577
|
+
const port = config.port ?? config.discoveryPort ?? this.options.discoveryPort;
|
|
578
|
+
const bindAddress = config.bindAddress ?? (process.platform === 'win32' ? undefined : group);
|
|
579
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
580
|
+
this.multicastDiscoverySocket = socket;
|
|
581
|
+
socket.on('message', (message, remote) => this.#onMulticastDiscoveryMessage(message, remote));
|
|
582
|
+
socket.on('error', error => this.emit('error', error));
|
|
583
|
+
|
|
584
|
+
await new Promise((resolve, reject) => {
|
|
585
|
+
const onError = error => {
|
|
586
|
+
socket.off('listening', onListening);
|
|
587
|
+
reject(error);
|
|
588
|
+
};
|
|
589
|
+
const onListening = () => {
|
|
590
|
+
socket.off('error', onError);
|
|
591
|
+
try {
|
|
592
|
+
const interfaces = multicastInterfaceCandidates(this.interfaceAddress);
|
|
593
|
+
if (interfaces.length) {
|
|
594
|
+
let joined = 0;
|
|
595
|
+
let firstError;
|
|
596
|
+
for (const interfaceAddress of interfaces) {
|
|
597
|
+
try {
|
|
598
|
+
socket.addMembership(group, interfaceAddress);
|
|
599
|
+
joined += 1;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
firstError ??= error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!joined) {
|
|
605
|
+
throw firstError ?? new Error(`could not join multicast group ${group}`);
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
socket.addMembership(group);
|
|
609
|
+
}
|
|
610
|
+
socket.setMulticastLoopback(true);
|
|
611
|
+
if (this.interfaceAddress) {
|
|
612
|
+
socket.setMulticastInterface(this.interfaceAddress);
|
|
613
|
+
}
|
|
614
|
+
this.#sendMulticastDiscoveryBeam();
|
|
615
|
+
const period = Math.max(100, Number(config.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS));
|
|
616
|
+
this.multicastDiscoveryTimer = setInterval(() => this.#sendMulticastDiscoveryBeam(), period);
|
|
617
|
+
this.multicastDiscoveryTimer.unref?.();
|
|
618
|
+
this.emit('multicastDiscoveryStart', { group, port });
|
|
619
|
+
resolve();
|
|
620
|
+
} catch (error) {
|
|
621
|
+
reject(error);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
socket.once('error', onError);
|
|
625
|
+
socket.once('listening', onListening);
|
|
626
|
+
socket.bind(port, bindAddress);
|
|
299
627
|
});
|
|
628
|
+
}
|
|
300
629
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.
|
|
305
|
-
|
|
630
|
+
#discoveryBeamBuffer({ padded = false } = {}) {
|
|
631
|
+
const beam = encodeSessionPresenceBeam({
|
|
632
|
+
protocolVersion: this.options.etherProtocolVersion,
|
|
633
|
+
info: this.processInfo,
|
|
634
|
+
beamPeriodNs: BigInt(Math.max(100, Number(this.options.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS))) * 1_000_000n,
|
|
635
|
+
endpoints: [this.listenEndpoint]
|
|
306
636
|
});
|
|
637
|
+
return padded ? padDiscoveryBeam(beam) : beam;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#sendDiscoveryBeam() {
|
|
641
|
+
if (!this.discoverySocket || this.discoverySocket.destroyed || !this.listenEndpoint) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
this.discoverySocket.write(this.#discoveryBeamBuffer({ padded: true }), error => {
|
|
645
|
+
if (error) {
|
|
646
|
+
this.emit('error', error);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#sendMulticastDiscoveryBeam() {
|
|
652
|
+
const socket = this.multicastDiscoverySocket;
|
|
653
|
+
if (!socket || !this.listenEndpoint) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const group = this.options.group ?? DEFAULT_DISCOVERY_GROUP;
|
|
657
|
+
const port = this.options.port ?? this.options.discoveryPort;
|
|
658
|
+
const beam = this.#discoveryBeamBuffer();
|
|
659
|
+
socket.send(beam, port, group, error => {
|
|
660
|
+
if (error) {
|
|
661
|
+
this.emit('error', error);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#onDiscoveryData(chunk) {
|
|
667
|
+
this.discoveryReceiveBuffer = Buffer.concat([this.discoveryReceiveBuffer, chunk]);
|
|
668
|
+
while (this.discoveryReceiveBuffer.length >= TCP_DISCOVERY_BEAM_SIZE) {
|
|
669
|
+
const message = this.discoveryReceiveBuffer.subarray(0, TCP_DISCOVERY_BEAM_SIZE);
|
|
670
|
+
this.discoveryReceiveBuffer = this.discoveryReceiveBuffer.subarray(TCP_DISCOVERY_BEAM_SIZE);
|
|
671
|
+
this.#onDiscoveryBeam(message);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#onDiscoveryBeam(message) {
|
|
676
|
+
let beam;
|
|
677
|
+
try {
|
|
678
|
+
beam = decodeSessionPresenceBeam(message);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
this.emit('decodeError', error, message);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (beam.info.sessionId !== this.processInfo.sessionId || isSameProcessInfo(beam.info, this.processInfo)) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const key = processKeyFromInfo(beam.info);
|
|
687
|
+
this.emit('beam', beam);
|
|
688
|
+
if (this.connectionsByProcessKey.has(key)) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const endpoint = beam.endpoints?.[0];
|
|
692
|
+
if (!endpoint) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.connect({ ...beam, info: beam.info, endpoints: beam.endpoints }).catch(error => this.emit('warning', error));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#onMulticastDiscoveryMessage(message, remote) {
|
|
699
|
+
let beam;
|
|
700
|
+
try {
|
|
701
|
+
beam = decodeSessionPresenceBeam(message);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
this.emit('decodeError', error, message, remote);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (beam.info.sessionId !== this.processInfo.sessionId || isSameProcessInfo(beam.info, this.processInfo)) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const key = processKeyFromInfo(beam.info);
|
|
710
|
+
this.emit('beam', beam);
|
|
711
|
+
if (this.connectionsByProcessKey.has(key)) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!beam.endpoints?.length) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
this.connect({ ...beam, info: beam.info, endpoints: beam.endpoints }).catch(error => this.emit('warning', error));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Connect to one endpoint from a SessionPresenceBeam process entry.
|
|
722
|
+
*
|
|
723
|
+
* @param {{ endpoints?: Array<{ host: string, port: number }>, info?: object } | { host: string, port: number }} target
|
|
724
|
+
*/
|
|
725
|
+
async connect(target) {
|
|
726
|
+
const endpoint = target.host ? target : target.endpoints?.[0];
|
|
727
|
+
if (!endpoint) {
|
|
728
|
+
throw new TypeError('SEN ether target must contain host/port or at least one endpoint');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!this.udpSocket) {
|
|
732
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
733
|
+
this.udpSocket.on('message', message => this.#onBusFrame(message));
|
|
734
|
+
this.udpSocket.on('error', error => this.emit('error', error));
|
|
735
|
+
await new Promise((resolve, reject) => {
|
|
736
|
+
const onError = error => {
|
|
737
|
+
this.udpSocket?.off('listening', onListening);
|
|
738
|
+
reject(error);
|
|
739
|
+
};
|
|
740
|
+
const onListening = () => {
|
|
741
|
+
this.udpSocket?.off('error', onError);
|
|
742
|
+
resolve();
|
|
743
|
+
};
|
|
744
|
+
this.udpSocket.once('error', onError);
|
|
745
|
+
this.udpSocket.once('listening', onListening);
|
|
746
|
+
this.udpSocket.bind(0);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const socket = net.createConnection({ host: endpoint.host, port: endpoint.port });
|
|
751
|
+
const connection = this.#registerSocket(socket, { target, incoming: false });
|
|
307
752
|
|
|
308
753
|
await new Promise((resolve, reject) => {
|
|
309
754
|
const onError = error => {
|
|
310
|
-
|
|
755
|
+
socket.off('connect', onConnect);
|
|
311
756
|
reject(error);
|
|
312
757
|
};
|
|
313
758
|
const onConnect = () => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
this.#configureTcpSocket();
|
|
759
|
+
socket.off('error', onError);
|
|
760
|
+
socket.on('error', error => this.emit('error', error));
|
|
761
|
+
this.#configureTcpSocket(socket);
|
|
317
762
|
try {
|
|
318
|
-
this.#sendHello();
|
|
763
|
+
this.#sendHello(connection);
|
|
319
764
|
resolve();
|
|
320
765
|
} catch (error) {
|
|
321
766
|
reject(error);
|
|
322
767
|
}
|
|
323
768
|
};
|
|
324
|
-
|
|
325
|
-
|
|
769
|
+
socket.once('error', onError);
|
|
770
|
+
socket.once('connect', onConnect);
|
|
326
771
|
});
|
|
327
772
|
|
|
328
773
|
return this;
|
|
329
774
|
}
|
|
330
775
|
|
|
331
|
-
#
|
|
776
|
+
#registerSocket(socket, metadata = {}) {
|
|
777
|
+
const connection = {
|
|
778
|
+
id: this.nextConnectionId++,
|
|
779
|
+
socket,
|
|
780
|
+
incoming: Boolean(metadata.incoming),
|
|
781
|
+
target: metadata.target,
|
|
782
|
+
receiveBuffer: Buffer.alloc(0),
|
|
783
|
+
remoteProcessInfo: metadata.target?.info,
|
|
784
|
+
ready: false
|
|
785
|
+
};
|
|
786
|
+
this.connections.set(connection.id, connection);
|
|
332
787
|
if (!this.socket) {
|
|
788
|
+
this.socket = socket;
|
|
789
|
+
}
|
|
790
|
+
socket.on('data', chunk => this.#onTcpData(connection, chunk));
|
|
791
|
+
socket.on('close', hadError => {
|
|
792
|
+
this.#removeConnection(connection);
|
|
793
|
+
this.emit('connectionClose', { connection, hadError });
|
|
794
|
+
if (!this.connections.size) {
|
|
795
|
+
this.ready = false;
|
|
796
|
+
this.emit('close', hadError);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
return connection;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
#removeConnection(connection) {
|
|
803
|
+
this.connections.delete(connection.id);
|
|
804
|
+
if (connection.processKey) {
|
|
805
|
+
this.connectionsByProcessKey.delete(connection.processKey);
|
|
806
|
+
}
|
|
807
|
+
for (const [busId, participants] of this.remoteParticipantsByBusId) {
|
|
808
|
+
for (const [participantId, participant] of participants) {
|
|
809
|
+
if (participant.connection === connection) {
|
|
810
|
+
participants.delete(participantId);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!participants.size) {
|
|
814
|
+
this.remoteParticipantsByBusId.delete(busId);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
for (const busState of this.buses.values()) {
|
|
818
|
+
for (const [key, interest] of busState.remoteInterests) {
|
|
819
|
+
if (interest.connection === connection) {
|
|
820
|
+
busState.remoteInterests.delete(key);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (this.socket === connection.socket) {
|
|
825
|
+
this.socket = [...this.connections.values()][0]?.socket;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#configureTcpSocket(socket) {
|
|
830
|
+
if (!socket) {
|
|
333
831
|
return;
|
|
334
832
|
}
|
|
335
833
|
if (this.options.socketKeepAlive !== false) {
|
|
336
|
-
|
|
834
|
+
socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
|
|
337
835
|
}
|
|
338
836
|
if (this.options.socketIdleTimeoutMs > 0) {
|
|
339
|
-
|
|
837
|
+
socket.setTimeout(this.options.socketIdleTimeoutMs, () => {
|
|
340
838
|
const error = new Error(`SEN ether TCP socket idle timeout after ${this.options.socketIdleTimeoutMs}ms`);
|
|
341
839
|
error.code = 'SEN_TCP_IDLE_TIMEOUT';
|
|
342
|
-
|
|
840
|
+
socket.destroy(error);
|
|
343
841
|
});
|
|
344
842
|
}
|
|
345
843
|
}
|
|
346
844
|
|
|
347
845
|
async close() {
|
|
348
|
-
const
|
|
846
|
+
const sockets = [...this.connections.values()].map(connection => connection.socket);
|
|
847
|
+
this.connections.clear();
|
|
848
|
+
this.connectionsByProcessKey.clear();
|
|
349
849
|
this.socket = undefined;
|
|
350
850
|
const udpSocket = this.udpSocket;
|
|
351
851
|
this.udpSocket = undefined;
|
|
852
|
+
const server = this.server;
|
|
853
|
+
this.server = undefined;
|
|
854
|
+
const discoverySocket = this.discoverySocket;
|
|
855
|
+
this.discoverySocket = undefined;
|
|
856
|
+
const multicastDiscoverySocket = this.multicastDiscoverySocket;
|
|
857
|
+
this.multicastDiscoverySocket = undefined;
|
|
858
|
+
if (this.discoveryTimer) {
|
|
859
|
+
clearInterval(this.discoveryTimer);
|
|
860
|
+
this.discoveryTimer = undefined;
|
|
861
|
+
}
|
|
862
|
+
if (this.multicastDiscoveryTimer) {
|
|
863
|
+
clearInterval(this.multicastDiscoveryTimer);
|
|
864
|
+
this.multicastDiscoveryTimer = undefined;
|
|
865
|
+
}
|
|
352
866
|
|
|
353
867
|
const closing = [];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
868
|
+
for (const socket of sockets) {
|
|
869
|
+
if (socket && !socket.destroyed) {
|
|
870
|
+
closing.push(withCloseTimeout(resolve => {
|
|
871
|
+
socket.once('close', resolve);
|
|
872
|
+
socket.destroy();
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (server) {
|
|
877
|
+
server.closeAllConnections?.();
|
|
878
|
+
closing.push(withCloseTimeout(resolve => {
|
|
879
|
+
server.close(resolve);
|
|
880
|
+
}));
|
|
881
|
+
}
|
|
882
|
+
if (discoverySocket && !discoverySocket.destroyed) {
|
|
883
|
+
closing.push(withCloseTimeout(resolve => {
|
|
884
|
+
discoverySocket.once('close', resolve);
|
|
885
|
+
discoverySocket.destroy();
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
if (multicastDiscoverySocket) {
|
|
889
|
+
closing.push(withCloseTimeout(resolve => {
|
|
890
|
+
multicastDiscoverySocket.close(resolve);
|
|
358
891
|
}));
|
|
359
892
|
}
|
|
360
893
|
if (udpSocket) {
|
|
361
|
-
closing.push(
|
|
894
|
+
closing.push(withCloseTimeout(resolve => {
|
|
362
895
|
udpSocket.close(resolve);
|
|
363
896
|
}));
|
|
364
897
|
}
|
|
365
898
|
for (const busState of this.buses.values()) {
|
|
366
899
|
if (busState.multicastSocket) {
|
|
367
|
-
closing.push(
|
|
900
|
+
closing.push(withCloseTimeout(resolve => {
|
|
368
901
|
busState.multicastSocket.close(resolve);
|
|
369
902
|
}));
|
|
370
903
|
}
|
|
@@ -381,11 +914,21 @@ export class EtherClient extends EventEmitter {
|
|
|
381
914
|
* @param {{ participantId?: number }} [options]
|
|
382
915
|
*/
|
|
383
916
|
async joinBus(busName, options = {}) {
|
|
384
|
-
if (!this.
|
|
385
|
-
throw new Error('EtherClient is not connected');
|
|
917
|
+
if (!this.connections.size && !this.server) {
|
|
918
|
+
throw new Error('EtherClient is not connected or started');
|
|
386
919
|
}
|
|
387
920
|
|
|
388
921
|
const busId = crc32(busName);
|
|
922
|
+
const existing = this.buses.get(busId);
|
|
923
|
+
if (existing) {
|
|
924
|
+
return {
|
|
925
|
+
busName: existing.busName,
|
|
926
|
+
busId: existing.busId,
|
|
927
|
+
participantId: existing.participantId,
|
|
928
|
+
multicastGroup: existing.multicastGroup,
|
|
929
|
+
multicastPort: this.options.busMulticastPort
|
|
930
|
+
};
|
|
931
|
+
}
|
|
389
932
|
const participantId = options.participantId ?? randomUInt32();
|
|
390
933
|
const bus = {
|
|
391
934
|
busName,
|
|
@@ -393,6 +936,10 @@ export class EtherClient extends EventEmitter {
|
|
|
393
936
|
participantId,
|
|
394
937
|
readyRemoteParticipants: new Set(),
|
|
395
938
|
interests: new Map(),
|
|
939
|
+
remoteInterests: new Map(),
|
|
940
|
+
publishedObjects: new Map(),
|
|
941
|
+
localTypeRegistry: new Map(),
|
|
942
|
+
localTypeResponsesByHash: new Map(),
|
|
396
943
|
multicastSocket: undefined,
|
|
397
944
|
multicastGroup: undefined
|
|
398
945
|
};
|
|
@@ -407,7 +954,7 @@ export class EtherClient extends EventEmitter {
|
|
|
407
954
|
throw error;
|
|
408
955
|
}
|
|
409
956
|
|
|
410
|
-
this.#
|
|
957
|
+
this.#sendControlPayloadToAll(encodeEtherControlMessage({
|
|
411
958
|
type: 'BusJoined',
|
|
412
959
|
value: {
|
|
413
960
|
participantId,
|
|
@@ -416,6 +963,13 @@ export class EtherClient extends EventEmitter {
|
|
|
416
963
|
}
|
|
417
964
|
}));
|
|
418
965
|
|
|
966
|
+
for (const participant of this.#remoteParticipantsForBus(bus.busId)) {
|
|
967
|
+
this.#sendBusControlToConnection(bus, participant.connection, {
|
|
968
|
+
type: 'RemoteParticipantReady',
|
|
969
|
+
value: { id: participant.id }
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
419
973
|
this.emit('busJoinedLocal', {
|
|
420
974
|
busName,
|
|
421
975
|
busId,
|
|
@@ -444,8 +998,8 @@ export class EtherClient extends EventEmitter {
|
|
|
444
998
|
*/
|
|
445
999
|
startInterest(bus, query, options = {}) {
|
|
446
1000
|
const busState = this.#getBus(bus);
|
|
447
|
-
const id = options.id ??
|
|
448
|
-
this.#
|
|
1001
|
+
const id = options.id ?? this.#nextInterestId(busState);
|
|
1002
|
+
this.#sendBusControlToRemoteParticipants(busState, {
|
|
449
1003
|
type: 'InterestStarted',
|
|
450
1004
|
value: { query, id }
|
|
451
1005
|
});
|
|
@@ -454,9 +1008,32 @@ export class EtherClient extends EventEmitter {
|
|
|
454
1008
|
return { busName: busState.busName, busId: busState.busId, id, query };
|
|
455
1009
|
}
|
|
456
1010
|
|
|
1011
|
+
#nextInterestId(busState) {
|
|
1012
|
+
for (let attempts = 0; attempts < 0xffff_ffff; attempts += 1) {
|
|
1013
|
+
this.nextInterestId = (this.nextInterestId + 1) >>> 0;
|
|
1014
|
+
const id = this.nextInterestId || 1;
|
|
1015
|
+
if (!busState.interests.has(id)) {
|
|
1016
|
+
this.nextInterestId = id;
|
|
1017
|
+
return id;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
throw new Error('could not allocate a SEN interest id');
|
|
1021
|
+
}
|
|
1022
|
+
|
|
457
1023
|
#restartInterestForRemote(busState) {
|
|
1024
|
+
for (const participant of this.#remoteParticipantsForBus(busState.busId)) {
|
|
1025
|
+
for (const interest of busState.interests.values()) {
|
|
1026
|
+
this.#sendBusControlToConnection(busState, participant.connection, {
|
|
1027
|
+
type: 'InterestStarted',
|
|
1028
|
+
value: { query: interest.query, id: interest.id }
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
#restartInterestForConnection(busState, connection) {
|
|
458
1035
|
for (const interest of busState.interests.values()) {
|
|
459
|
-
this.#
|
|
1036
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
460
1037
|
type: 'InterestStarted',
|
|
461
1038
|
value: { query: interest.query, id: interest.id }
|
|
462
1039
|
});
|
|
@@ -472,7 +1049,7 @@ export class EtherClient extends EventEmitter {
|
|
|
472
1049
|
stopInterest(bus, id) {
|
|
473
1050
|
const busState = this.#getBus(bus);
|
|
474
1051
|
busState.interests.delete(id);
|
|
475
|
-
this.#
|
|
1052
|
+
this.#sendBusControlToRemoteParticipants(busState, {
|
|
476
1053
|
type: 'InterestStopped',
|
|
477
1054
|
value: { id }
|
|
478
1055
|
});
|
|
@@ -491,7 +1068,7 @@ export class EtherClient extends EventEmitter {
|
|
|
491
1068
|
return { busName: busState.busName, busId: busState.busId, requests };
|
|
492
1069
|
}
|
|
493
1070
|
|
|
494
|
-
this.#
|
|
1071
|
+
this.#sendBusControlToRemoteParticipants(busState, {
|
|
495
1072
|
type: 'TypesInfoRequest',
|
|
496
1073
|
value: {
|
|
497
1074
|
ownerId: busState.participantId,
|
|
@@ -521,7 +1098,7 @@ export class EtherClient extends EventEmitter {
|
|
|
521
1098
|
return { busName: busState.busName, busId: busState.busId, requests: normalized };
|
|
522
1099
|
}
|
|
523
1100
|
|
|
524
|
-
this.#
|
|
1101
|
+
this.#sendBusControlToRemoteParticipants(busState, {
|
|
525
1102
|
type: 'ObjectsStateRequest',
|
|
526
1103
|
value: {
|
|
527
1104
|
ownerId: busState.participantId,
|
|
@@ -532,6 +1109,75 @@ export class EtherClient extends EventEmitter {
|
|
|
532
1109
|
return { busName: busState.busName, busId: busState.busId, requests: normalized };
|
|
533
1110
|
}
|
|
534
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* Publish local JavaScript objects on a joined SEN bus.
|
|
1114
|
+
*
|
|
1115
|
+
* Objects need at least `{ name, className, properties }`. A `spec` can be
|
|
1116
|
+
* supplied for exact SEN typing; otherwise a simple ClassTypeSpec is inferred
|
|
1117
|
+
* from the current property values.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {string | number} bus Bus name or bus id.
|
|
1120
|
+
* @param {object|object[]} objects
|
|
1121
|
+
* @param {{ types?: Map<string, object>|Record<string, object>|object[] }} [options]
|
|
1122
|
+
*/
|
|
1123
|
+
publishObjects(bus, objects, options = {}) {
|
|
1124
|
+
const busState = this.#getBus(bus);
|
|
1125
|
+
const list = Array.isArray(objects) ? objects : [objects];
|
|
1126
|
+
const externalTypes = normalizeTypeDefinitions(options.types);
|
|
1127
|
+
for (const type of externalTypes) {
|
|
1128
|
+
this.#registerLocalType(busState, type);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const published = [];
|
|
1132
|
+
for (const item of list) {
|
|
1133
|
+
const localObject = buildLocalObject(item, busState.localTypeRegistry);
|
|
1134
|
+
busState.publishedObjects.set(localObject.id, localObject);
|
|
1135
|
+
this.#registerLocalType(busState, localObject.spec, localObject.typeHash);
|
|
1136
|
+
published.push(localObject);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (published.length) {
|
|
1140
|
+
this.#publishObjectsToRemoteInterests(busState, published);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
this.emit('objectsPublishedLocal', {
|
|
1144
|
+
busName: busState.busName,
|
|
1145
|
+
busId: busState.busId,
|
|
1146
|
+
objects: published
|
|
1147
|
+
});
|
|
1148
|
+
return published;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Remove previously published local objects from a joined bus.
|
|
1153
|
+
*
|
|
1154
|
+
* @param {string | number} bus Bus name or bus id.
|
|
1155
|
+
* @param {Array<string|number>|string|number} objects Object ids or names.
|
|
1156
|
+
*/
|
|
1157
|
+
removePublishedObjects(bus, objects) {
|
|
1158
|
+
const busState = this.#getBus(bus);
|
|
1159
|
+
const selectors = Array.isArray(objects) ? objects : [objects];
|
|
1160
|
+
const removed = [];
|
|
1161
|
+
for (const selector of selectors) {
|
|
1162
|
+
const id = typeof selector === 'number'
|
|
1163
|
+
? selector >>> 0
|
|
1164
|
+
: [...busState.publishedObjects.values()].find(object => object.name === selector)?.id;
|
|
1165
|
+
if (id === undefined) continue;
|
|
1166
|
+
if (busState.publishedObjects.delete(id)) removed.push(id);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (removed.length) {
|
|
1170
|
+
this.#removeObjectsFromRemoteInterests(busState, removed);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
this.emit('objectsRemovedLocal', {
|
|
1174
|
+
busName: busState.busName,
|
|
1175
|
+
busId: busState.busId,
|
|
1176
|
+
objectIds: removed
|
|
1177
|
+
});
|
|
1178
|
+
return removed;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
535
1181
|
/**
|
|
536
1182
|
* Send a runtime method call to a remote participant on a joined bus.
|
|
537
1183
|
*
|
|
@@ -546,6 +1192,7 @@ export class EtherClient extends EventEmitter {
|
|
|
546
1192
|
*/
|
|
547
1193
|
sendRuntimeMethodCall(bus, call) {
|
|
548
1194
|
const busState = this.#getBus(bus);
|
|
1195
|
+
const remote = this.#remoteParticipantForBus(busState.busId, call.to);
|
|
549
1196
|
const message = encodeRuntimeMethodCall({
|
|
550
1197
|
ownerId: busState.participantId,
|
|
551
1198
|
objectId: call.objectId,
|
|
@@ -554,7 +1201,7 @@ export class EtherClient extends EventEmitter {
|
|
|
554
1201
|
confirmed: call.confirmed,
|
|
555
1202
|
argumentsBuffer: call.argumentsBuffer
|
|
556
1203
|
});
|
|
557
|
-
this.#
|
|
1204
|
+
this.#sendBusMessageToConnection(busState, remote?.connection, message);
|
|
558
1205
|
this.emit('runtimeMethodCallSent', {
|
|
559
1206
|
busName: busState.busName,
|
|
560
1207
|
busId: busState.busId,
|
|
@@ -577,7 +1224,7 @@ export class EtherClient extends EventEmitter {
|
|
|
577
1224
|
this.stopInterest(busState.busId, id);
|
|
578
1225
|
}
|
|
579
1226
|
|
|
580
|
-
this.#
|
|
1227
|
+
this.#sendControlPayloadToAll(encodeEtherControlMessage({
|
|
581
1228
|
type: 'BusLeft',
|
|
582
1229
|
value: {
|
|
583
1230
|
participantId: busState.participantId,
|
|
@@ -597,7 +1244,7 @@ export class EtherClient extends EventEmitter {
|
|
|
597
1244
|
});
|
|
598
1245
|
}
|
|
599
1246
|
|
|
600
|
-
#sendHello() {
|
|
1247
|
+
#sendHello(connection) {
|
|
601
1248
|
const udpPort = this.udpSocket?.address()?.port;
|
|
602
1249
|
const payload = encodeEtherControlMessage({
|
|
603
1250
|
type: 'Hello',
|
|
@@ -610,15 +1257,15 @@ export class EtherClient extends EventEmitter {
|
|
|
610
1257
|
}
|
|
611
1258
|
}
|
|
612
1259
|
});
|
|
613
|
-
this.#
|
|
1260
|
+
this.#sendControlPayloadToConnection(connection, payload);
|
|
614
1261
|
}
|
|
615
1262
|
|
|
616
|
-
#sendReady() {
|
|
617
|
-
this.#
|
|
1263
|
+
#sendReady(connection) {
|
|
1264
|
+
this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({ type: 'Ready' }));
|
|
618
1265
|
}
|
|
619
1266
|
|
|
620
|
-
#
|
|
621
|
-
const socket = this.#
|
|
1267
|
+
#sendControlPayloadToConnection(connection, payload) {
|
|
1268
|
+
const socket = this.#writableConnectionSocket(connection);
|
|
622
1269
|
socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.controlMessage, payload), error => {
|
|
623
1270
|
if (error) {
|
|
624
1271
|
this.emit('error', error);
|
|
@@ -626,88 +1273,158 @@ export class EtherClient extends EventEmitter {
|
|
|
626
1273
|
});
|
|
627
1274
|
}
|
|
628
1275
|
|
|
629
|
-
#
|
|
630
|
-
|
|
1276
|
+
#sendControlPayloadToAll(payload) {
|
|
1277
|
+
for (const connection of this.connections.values()) {
|
|
1278
|
+
this.#sendControlPayloadToConnection(connection, payload);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
#onTcpData(connection, chunk) {
|
|
1283
|
+
connection.receiveBuffer = Buffer.concat([connection.receiveBuffer, chunk]);
|
|
631
1284
|
|
|
632
|
-
while (
|
|
633
|
-
const header = decodeProcessTcpHeader(
|
|
1285
|
+
while (connection.receiveBuffer.length >= 5) {
|
|
1286
|
+
const header = decodeProcessTcpHeader(connection.receiveBuffer);
|
|
634
1287
|
const frameSize = 5 + header.payloadSize;
|
|
635
|
-
if (
|
|
1288
|
+
if (connection.receiveBuffer.length < frameSize) {
|
|
636
1289
|
return;
|
|
637
1290
|
}
|
|
638
1291
|
|
|
639
|
-
const payload =
|
|
640
|
-
|
|
641
|
-
this.#onFrame(header.category, payload);
|
|
1292
|
+
const payload = connection.receiveBuffer.subarray(5, frameSize);
|
|
1293
|
+
connection.receiveBuffer = connection.receiveBuffer.subarray(frameSize);
|
|
1294
|
+
this.#onFrame(connection, header.category, payload);
|
|
642
1295
|
}
|
|
643
1296
|
}
|
|
644
1297
|
|
|
645
|
-
#onFrame(category, payload) {
|
|
1298
|
+
#onFrame(connection, category, payload) {
|
|
646
1299
|
if (category === PROCESS_MESSAGE_CATEGORY.controlMessage) {
|
|
647
1300
|
const message = decodeEtherControlMessage(payload);
|
|
648
|
-
this.emit('controlMessage', message);
|
|
649
|
-
this.#onControlMessage(message);
|
|
1301
|
+
this.emit('controlMessage', message, connection);
|
|
1302
|
+
this.#onControlMessage(connection, message);
|
|
650
1303
|
return;
|
|
651
1304
|
}
|
|
652
1305
|
|
|
653
1306
|
if (category === PROCESS_MESSAGE_CATEGORY.busMessage) {
|
|
654
|
-
this.#onBusFrame(payload);
|
|
1307
|
+
this.#onBusFrame(payload, connection);
|
|
655
1308
|
return;
|
|
656
1309
|
}
|
|
657
1310
|
|
|
658
1311
|
this.emit('error', new RangeError(`unknown SEN process frame category: ${category}`));
|
|
659
1312
|
}
|
|
660
1313
|
|
|
661
|
-
#onControlMessage(message) {
|
|
1314
|
+
#onControlMessage(connection, message) {
|
|
662
1315
|
switch (message.type) {
|
|
663
1316
|
case 'Hello':
|
|
664
|
-
this.#onHello(message.value);
|
|
1317
|
+
this.#onHello(connection, message.value);
|
|
665
1318
|
break;
|
|
666
1319
|
case 'Ready':
|
|
1320
|
+
connection.ready = true;
|
|
667
1321
|
this.ready = true;
|
|
668
|
-
this.emit('ready',
|
|
1322
|
+
this.emit('ready', connection.remoteProcessInfo);
|
|
1323
|
+
this.emit('connectionReady', { connection, remoteProcessInfo: connection.remoteProcessInfo });
|
|
669
1324
|
break;
|
|
670
1325
|
case 'BusJoined':
|
|
671
|
-
this
|
|
1326
|
+
this.#onRemoteBusJoined(connection, message.value);
|
|
672
1327
|
break;
|
|
673
1328
|
case 'BusLeft':
|
|
674
|
-
this
|
|
1329
|
+
this.#onRemoteBusLeft(connection, message.value);
|
|
675
1330
|
break;
|
|
676
1331
|
default:
|
|
677
1332
|
this.emit('error', new RangeError(`unknown SEN ether control message: ${message.type}`));
|
|
678
1333
|
}
|
|
679
1334
|
}
|
|
680
1335
|
|
|
681
|
-
#onHello(hello) {
|
|
1336
|
+
#onHello(connection, hello) {
|
|
682
1337
|
try {
|
|
683
1338
|
validateRemoteHello(hello, this.options, this.processInfo);
|
|
684
1339
|
} catch (error) {
|
|
685
|
-
|
|
1340
|
+
connection.socket?.destroy(error);
|
|
686
1341
|
return;
|
|
687
1342
|
}
|
|
688
1343
|
|
|
689
|
-
|
|
1344
|
+
connection.remoteProcessInfo = hello.info;
|
|
1345
|
+
connection.processKey = processKeyFromInfo(hello.info);
|
|
1346
|
+
this.connectionsByProcessKey.set(connection.processKey, connection);
|
|
1347
|
+
this.remoteProcessInfo ??= hello.info;
|
|
690
1348
|
this.emit('remoteProcess', hello);
|
|
691
1349
|
try {
|
|
692
|
-
this.#sendReady();
|
|
1350
|
+
this.#sendReady(connection);
|
|
1351
|
+
this.#announceLocalBusesToConnection(connection);
|
|
693
1352
|
} catch (error) {
|
|
694
1353
|
this.emit('error', error);
|
|
695
1354
|
}
|
|
696
1355
|
}
|
|
697
1356
|
|
|
698
|
-
#
|
|
1357
|
+
#announceLocalBusesToConnection(connection) {
|
|
1358
|
+
for (const busState of this.buses.values()) {
|
|
1359
|
+
this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({
|
|
1360
|
+
type: 'BusJoined',
|
|
1361
|
+
value: {
|
|
1362
|
+
participantId: busState.participantId,
|
|
1363
|
+
busId: busState.busId,
|
|
1364
|
+
busName: busState.busName
|
|
1365
|
+
}
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
#onRemoteBusJoined(connection, value) {
|
|
1371
|
+
const participant = {
|
|
1372
|
+
id: value.participantId >>> 0,
|
|
1373
|
+
busId: value.busId >>> 0,
|
|
1374
|
+
busName: value.busName,
|
|
1375
|
+
connection
|
|
1376
|
+
};
|
|
1377
|
+
let participants = this.remoteParticipantsByBusId.get(participant.busId);
|
|
1378
|
+
if (!participants) {
|
|
1379
|
+
participants = new Map();
|
|
1380
|
+
this.remoteParticipantsByBusId.set(participant.busId, participants);
|
|
1381
|
+
}
|
|
1382
|
+
participants.set(participant.id, participant);
|
|
1383
|
+
this.emit('busJoined', { ...value, connection });
|
|
1384
|
+
|
|
1385
|
+
const busState = this.buses.get(participant.busId);
|
|
1386
|
+
if (busState) {
|
|
1387
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1388
|
+
type: 'RemoteParticipantReady',
|
|
1389
|
+
value: { id: participant.id }
|
|
1390
|
+
});
|
|
1391
|
+
this.#restartInterestForConnection(busState, connection);
|
|
1392
|
+
this.#publishObjectsToRemoteInterests(busState, [...busState.publishedObjects.values()]);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
#onRemoteBusLeft(connection, value) {
|
|
1397
|
+
const busId = value.busId >>> 0;
|
|
1398
|
+
const participantId = value.participantId >>> 0;
|
|
1399
|
+
const participants = this.remoteParticipantsByBusId.get(busId);
|
|
1400
|
+
participants?.delete(participantId);
|
|
1401
|
+
if (participants && !participants.size) {
|
|
1402
|
+
this.remoteParticipantsByBusId.delete(busId);
|
|
1403
|
+
}
|
|
1404
|
+
const busState = this.buses.get(busId);
|
|
1405
|
+
if (busState) {
|
|
1406
|
+
for (const [key, interest] of busState.remoteInterests) {
|
|
1407
|
+
if (interest.connection === connection && interest.participantId === participantId) {
|
|
1408
|
+
busState.remoteInterests.delete(key);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
this.emit('busLeft', { ...value, connection });
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
#onBusFrame(payload, connection = undefined) {
|
|
699
1416
|
const frame = decodeConfirmedBusFrame(payload);
|
|
700
1417
|
const busMessage = decodeBusMessage(frame.message);
|
|
701
|
-
this.emit('busFrame', { ...frame, busMessage });
|
|
1418
|
+
this.emit('busFrame', { ...frame, busMessage, connection });
|
|
702
1419
|
|
|
703
1420
|
if (busMessage.categoryName !== 'controlMessage') {
|
|
704
|
-
this.emit(busMessage.categoryName, { ...frame, ...busMessage });
|
|
1421
|
+
this.emit(busMessage.categoryName, { ...frame, ...busMessage, connection });
|
|
705
1422
|
return;
|
|
706
1423
|
}
|
|
707
1424
|
|
|
708
1425
|
const busState = this.buses.get(frame.busId);
|
|
709
1426
|
const control = busMessage.control;
|
|
710
|
-
this.emit('busControlMessage', { ...frame, control });
|
|
1427
|
+
this.emit('busControlMessage', { ...frame, control, connection });
|
|
711
1428
|
|
|
712
1429
|
if (!busState) {
|
|
713
1430
|
return;
|
|
@@ -715,22 +1432,34 @@ export class EtherClient extends EventEmitter {
|
|
|
715
1432
|
|
|
716
1433
|
switch (control.type) {
|
|
717
1434
|
case 'RemoteParticipantReady':
|
|
718
|
-
this.#onRemoteParticipantReady(busState, frame, control.value);
|
|
1435
|
+
this.#onRemoteParticipantReady(busState, frame, control.value, connection);
|
|
1436
|
+
break;
|
|
1437
|
+
case 'InterestStarted':
|
|
1438
|
+
this.#onRemoteInterestStarted(busState, frame, control.value, connection);
|
|
1439
|
+
break;
|
|
1440
|
+
case 'InterestStopped':
|
|
1441
|
+
this.#onRemoteInterestStopped(busState, frame, control.value, connection);
|
|
719
1442
|
break;
|
|
720
1443
|
case 'ObjectsPublished':
|
|
721
|
-
this.emit('objectsPublished', { bus: busState, ...control.value });
|
|
1444
|
+
this.emit('objectsPublished', { bus: busState, connection, ...control.value });
|
|
722
1445
|
break;
|
|
723
1446
|
case 'ObjectsRemoved':
|
|
724
|
-
this.emit('objectsRemoved', { bus: busState, ...control.value });
|
|
1447
|
+
this.emit('objectsRemoved', { bus: busState, connection, ...control.value });
|
|
725
1448
|
break;
|
|
726
1449
|
case 'ObjectsStateResponse':
|
|
727
|
-
this.emit('objectsStateResponse', { bus: busState, ...control.value });
|
|
1450
|
+
this.emit('objectsStateResponse', { bus: busState, connection, ...control.value });
|
|
728
1451
|
break;
|
|
729
1452
|
case 'TypesInfoResponse':
|
|
730
|
-
this.emit('typesInfoResponse', { bus: busState, ...control.value });
|
|
1453
|
+
this.emit('typesInfoResponse', { bus: busState, connection, ...control.value });
|
|
731
1454
|
break;
|
|
732
1455
|
case 'TypesInfoRejection':
|
|
733
|
-
this.emit('typesInfoRejection', { bus: busState, ...control.value });
|
|
1456
|
+
this.emit('typesInfoRejection', { bus: busState, connection, ...control.value });
|
|
1457
|
+
break;
|
|
1458
|
+
case 'ObjectsStateRequest':
|
|
1459
|
+
this.#onObjectsStateRequest(busState, frame, control.value, connection);
|
|
1460
|
+
break;
|
|
1461
|
+
case 'TypesInfoRequest':
|
|
1462
|
+
this.#onTypesInfoRequest(busState, frame, control.value, connection);
|
|
734
1463
|
break;
|
|
735
1464
|
default:
|
|
736
1465
|
break;
|
|
@@ -807,7 +1536,7 @@ export class EtherClient extends EventEmitter {
|
|
|
807
1536
|
});
|
|
808
1537
|
}
|
|
809
1538
|
|
|
810
|
-
#onRemoteParticipantReady(busState, frame, value) {
|
|
1539
|
+
#onRemoteParticipantReady(busState, frame, value, connection) {
|
|
811
1540
|
if (value.id !== busState.participantId) {
|
|
812
1541
|
return;
|
|
813
1542
|
}
|
|
@@ -815,29 +1544,210 @@ export class EtherClient extends EventEmitter {
|
|
|
815
1544
|
const remoteParticipantId = frame.to >>> 0;
|
|
816
1545
|
if (!busState.readyRemoteParticipants.has(remoteParticipantId)) {
|
|
817
1546
|
busState.readyRemoteParticipants.add(remoteParticipantId);
|
|
818
|
-
this.#
|
|
1547
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
819
1548
|
type: 'RemoteParticipantReady',
|
|
820
1549
|
value: { id: remoteParticipantId }
|
|
821
1550
|
});
|
|
822
|
-
this.#
|
|
1551
|
+
this.#restartInterestForConnection(busState, connection);
|
|
823
1552
|
this.emit('busParticipantReady', {
|
|
824
1553
|
busName: busState.busName,
|
|
825
1554
|
busId: busState.busId,
|
|
826
1555
|
participantId: busState.participantId,
|
|
827
|
-
remoteParticipantId
|
|
1556
|
+
remoteParticipantId,
|
|
1557
|
+
connection
|
|
828
1558
|
});
|
|
829
1559
|
}
|
|
830
1560
|
}
|
|
831
1561
|
|
|
832
|
-
#
|
|
1562
|
+
#onRemoteInterestStarted(busState, frame, value, connection) {
|
|
1563
|
+
const remoteParticipantId = frame.to >>> 0;
|
|
1564
|
+
const id = value.id >>> 0;
|
|
1565
|
+
const key = `${connection?.id ?? 0}:${remoteParticipantId}:${id}`;
|
|
1566
|
+
busState.remoteInterests.set(key, {
|
|
1567
|
+
participantId: remoteParticipantId,
|
|
1568
|
+
connection,
|
|
1569
|
+
id,
|
|
1570
|
+
query: value.query
|
|
1571
|
+
});
|
|
1572
|
+
this.emit('remoteInterestStarted', {
|
|
1573
|
+
busName: busState.busName,
|
|
1574
|
+
busId: busState.busId,
|
|
1575
|
+
participantId: remoteParticipantId,
|
|
1576
|
+
connection,
|
|
1577
|
+
id,
|
|
1578
|
+
query: value.query
|
|
1579
|
+
});
|
|
1580
|
+
this.#publishObjectsToRemoteInterests(busState, [...busState.publishedObjects.values()], [key]);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
#onRemoteInterestStopped(busState, frame, value, connection) {
|
|
1584
|
+
const remoteParticipantId = frame.to >>> 0;
|
|
1585
|
+
const id = value.id >>> 0;
|
|
1586
|
+
busState.remoteInterests.delete(`${connection?.id ?? 0}:${remoteParticipantId}:${id}`);
|
|
1587
|
+
this.emit('remoteInterestStopped', {
|
|
1588
|
+
busName: busState.busName,
|
|
1589
|
+
busId: busState.busId,
|
|
1590
|
+
participantId: remoteParticipantId,
|
|
1591
|
+
connection,
|
|
1592
|
+
id
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
#onObjectsStateRequest(busState, frame, value, connection) {
|
|
1597
|
+
const remoteParticipantId = frame.to >>> 0;
|
|
1598
|
+
const responses = [];
|
|
1599
|
+
for (const request of value.requests ?? []) {
|
|
1600
|
+
const objectStates = [];
|
|
1601
|
+
for (const objectId of request.objectIds ?? []) {
|
|
1602
|
+
const object = busState.publishedObjects.get(objectId >>> 0);
|
|
1603
|
+
if (!object) continue;
|
|
1604
|
+
objectStates.push({
|
|
1605
|
+
id: object.id,
|
|
1606
|
+
timestamp: object.timestamp,
|
|
1607
|
+
state: object.stateBuffer
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (objectStates.length) {
|
|
1611
|
+
responses.push({ interestId: request.interestId, objectStates });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!responses.length) return;
|
|
1615
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1616
|
+
type: 'ObjectsStateResponse',
|
|
1617
|
+
value: {
|
|
1618
|
+
ownerId: busState.participantId,
|
|
1619
|
+
responses
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
#onTypesInfoRequest(busState, frame, value, connection) {
|
|
1625
|
+
const remoteParticipantId = frame.to >>> 0;
|
|
1626
|
+
const types = [];
|
|
1627
|
+
const rejections = [];
|
|
1628
|
+
for (const request of value.requests ?? []) {
|
|
1629
|
+
const type = busState.localTypeResponsesByHash.get(request >>> 0);
|
|
1630
|
+
if (type) {
|
|
1631
|
+
types.push(type);
|
|
1632
|
+
} else {
|
|
1633
|
+
rejections.push(String(request));
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (types.length) {
|
|
1637
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1638
|
+
type: 'TypesInfoResponse',
|
|
1639
|
+
value: {
|
|
1640
|
+
ownerId: busState.participantId,
|
|
1641
|
+
types
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
if (rejections.length) {
|
|
1646
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1647
|
+
type: 'TypesInfoRejection',
|
|
1648
|
+
value: {
|
|
1649
|
+
ownerId: busState.participantId,
|
|
1650
|
+
rejections
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
#registerLocalType(busState, spec, hash = crc32(spec?.qualifiedName ?? spec?.name ?? '')) {
|
|
1657
|
+
if (!spec?.qualifiedName) return;
|
|
1658
|
+
busState.localTypeRegistry.set(spec.qualifiedName, spec);
|
|
1659
|
+
const response = spec.data?.type === 'ClassTypeSpec'
|
|
1660
|
+
? {
|
|
1661
|
+
type: 'ClassSpecResponse',
|
|
1662
|
+
classHash: hash >>> 0,
|
|
1663
|
+
spec,
|
|
1664
|
+
dependentTypes: []
|
|
1665
|
+
}
|
|
1666
|
+
: {
|
|
1667
|
+
type: 'NonClassSpecResponse',
|
|
1668
|
+
spec
|
|
1669
|
+
};
|
|
1670
|
+
busState.localTypeResponsesByHash.set((hash >>> 0), response);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
#publishObjectsToRemoteInterests(busState, objects, keys = undefined) {
|
|
1674
|
+
if (!objects.length) return;
|
|
1675
|
+
const targets = (keys ?? [...busState.remoteInterests.keys()])
|
|
1676
|
+
.map(key => busState.remoteInterests.get(key))
|
|
1677
|
+
.filter(Boolean);
|
|
1678
|
+
if (!targets.length) return;
|
|
1679
|
+
|
|
1680
|
+
const byConnection = new Map();
|
|
1681
|
+
for (const interest of targets) {
|
|
1682
|
+
const connection = interest.connection;
|
|
1683
|
+
if (!connection) continue;
|
|
1684
|
+
const list = byConnection.get(connection) ?? [];
|
|
1685
|
+
list.push({
|
|
1686
|
+
interestId: interest.id,
|
|
1687
|
+
objects: objects.map(object => ({
|
|
1688
|
+
className: object.className,
|
|
1689
|
+
typeHash: object.typeHash,
|
|
1690
|
+
name: object.name,
|
|
1691
|
+
id: object.id,
|
|
1692
|
+
state: object.stateBuffer,
|
|
1693
|
+
time: object.timestamp
|
|
1694
|
+
}))
|
|
1695
|
+
});
|
|
1696
|
+
byConnection.set(connection, list);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
for (const [connection, discoveries] of byConnection) {
|
|
1700
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1701
|
+
type: 'ObjectsPublished',
|
|
1702
|
+
value: {
|
|
1703
|
+
ownerId: busState.participantId,
|
|
1704
|
+
discoveries
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
#removeObjectsFromRemoteInterests(busState, objectIds) {
|
|
1711
|
+
const targets = [...busState.remoteInterests.values()];
|
|
1712
|
+
if (!targets.length || !objectIds.length) return;
|
|
1713
|
+
const byConnection = new Map();
|
|
1714
|
+
for (const interest of targets) {
|
|
1715
|
+
const connection = interest.connection;
|
|
1716
|
+
if (!connection) continue;
|
|
1717
|
+
const removals = byConnection.get(connection) ?? [];
|
|
1718
|
+
removals.push({ interestId: interest.id, ids: objectIds });
|
|
1719
|
+
byConnection.set(connection, removals);
|
|
1720
|
+
}
|
|
1721
|
+
for (const [connection, removals] of byConnection) {
|
|
1722
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
1723
|
+
type: 'ObjectsRemoved',
|
|
1724
|
+
value: { removals }
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
#sendBusControlToRemoteParticipants(busState, message) {
|
|
1730
|
+
const connections = new Set(this.#remoteParticipantsForBus(busState.busId).map(participant => participant.connection));
|
|
1731
|
+
for (const connection of connections) {
|
|
1732
|
+
this.#sendBusControlToConnection(busState, connection, message);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
#sendBusControlToConnection(busState, connection, message) {
|
|
833
1737
|
const busPayload = encodeBusControlMessage(message);
|
|
834
|
-
this.#
|
|
1738
|
+
this.#sendBusMessageToConnection(busState, connection, busPayload);
|
|
835
1739
|
}
|
|
836
1740
|
|
|
837
|
-
#
|
|
838
|
-
|
|
1741
|
+
#sendBusMessageToConnection(busState, connection, busPayload) {
|
|
1742
|
+
if (!connection) {
|
|
1743
|
+
for (const participant of this.#remoteParticipantsForBus(busState.busId)) {
|
|
1744
|
+
this.#sendBusMessageToConnection(busState, participant.connection, busPayload);
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const socket = this.#writableConnectionSocket(connection);
|
|
839
1749
|
const processBusPayload = encodeConfirmedBusFrame({
|
|
840
|
-
to,
|
|
1750
|
+
to: busState.participantId,
|
|
841
1751
|
busId: busState.busId,
|
|
842
1752
|
message: busPayload
|
|
843
1753
|
});
|
|
@@ -848,14 +1758,23 @@ export class EtherClient extends EventEmitter {
|
|
|
848
1758
|
});
|
|
849
1759
|
}
|
|
850
1760
|
|
|
851
|
-
#
|
|
852
|
-
|
|
1761
|
+
#writableConnectionSocket(connection) {
|
|
1762
|
+
const socket = connection?.socket;
|
|
1763
|
+
if (!socket || socket.destroyed || !socket.writable) {
|
|
853
1764
|
const error = new Error('SEN ether TCP socket is not writable');
|
|
854
1765
|
error.code = 'SEN_TCP_NOT_WRITABLE';
|
|
855
1766
|
this.emit('error', error);
|
|
856
1767
|
throw error;
|
|
857
1768
|
}
|
|
858
|
-
return
|
|
1769
|
+
return socket;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
#remoteParticipantsForBus(busId) {
|
|
1773
|
+
return [...(this.remoteParticipantsByBusId.get(busId >>> 0)?.values() ?? [])];
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
#remoteParticipantForBus(busId, participantId) {
|
|
1777
|
+
return this.remoteParticipantsByBusId.get(busId >>> 0)?.get(participantId >>> 0);
|
|
859
1778
|
}
|
|
860
1779
|
|
|
861
1780
|
#getBus(bus) {
|