sen-ether-client 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,448 @@ 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();
271
473
  }
272
474
 
273
475
  /**
274
- * Connect to one endpoint from a SessionPresenceBeam process entry.
476
+ * Start this JS process as an active Ether node.
275
477
  *
276
- * @param {{ endpoints?: Array<{ host: string, port: number }>, info?: object } | { host: string, port: number }} target
478
+ * It opens a TCP listener for process-to-process traffic and, when `tcpHub`
479
+ * is configured, beams its presence to the hub while connecting to compatible
480
+ * remote processes announced by the hub.
277
481
  */
278
- async connect(target) {
279
- const endpoint = target.host ? target : target.endpoints?.[0];
280
- if (!endpoint) {
281
- throw new TypeError('SEN ether target must contain host/port or at least one endpoint');
482
+ async start(options = {}) {
483
+ const config = { ...this.options, ...options };
484
+ this.options = config;
485
+ this.interfaceAddress = resolveInterfaceAddress(this.options.interfaceAddress);
486
+ if (config.listen !== false && !this.server) {
487
+ await this.#startServer(config);
488
+ }
489
+ if (config.tcpHub && !this.discoverySocket) {
490
+ await this.#startTcpDiscovery(config);
282
491
  }
492
+ if (!config.tcpHub && config.multicastDiscovery !== false && !this.multicastDiscoverySocket) {
493
+ await this.#startMulticastDiscovery(config);
494
+ }
495
+ return this;
496
+ }
497
+
498
+ async #startServer(config) {
499
+ const server = net.createServer(socket => {
500
+ const connection = this.#registerSocket(socket, { incoming: true });
501
+ this.#configureTcpSocket(socket);
502
+ this.#sendHello(connection);
503
+ });
504
+ this.server = server;
505
+ server.on('error', error => this.emit('error', error));
283
506
 
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
507
  await new Promise((resolve, reject) => {
288
508
  const onError = error => {
289
- this.udpSocket?.off('listening', onListening);
509
+ server.off('listening', onListening);
290
510
  reject(error);
291
511
  };
292
512
  const onListening = () => {
293
- this.udpSocket?.off('error', onError);
513
+ server.off('error', onError);
514
+ const address = server.address();
515
+ const port = typeof address === 'object' && address ? address.port : config.listenPort;
516
+ const listenHost = config.listenHost ?? '0.0.0.0';
517
+ this.listenEndpoint = {
518
+ host: config.advertisedHost ?? (listenHost === '0.0.0.0' || listenHost === '::'
519
+ ? firstAdvertisableAddress(this.interfaceAddress)
520
+ : listenHost),
521
+ port
522
+ };
523
+ this.emit('listening', this.listenEndpoint);
294
524
  resolve();
295
525
  };
296
- this.udpSocket.once('error', onError);
297
- this.udpSocket.once('listening', onListening);
298
- this.udpSocket.bind(0);
526
+ server.once('error', onError);
527
+ server.once('listening', onListening);
528
+ server.listen(config.listenPort ?? 0, config.listenHost ?? '0.0.0.0');
529
+ });
530
+ }
531
+
532
+ async #startTcpDiscovery(config) {
533
+ const hub = parseHostPort(config.tcpHub, 64222);
534
+ if (!hub?.host || !Number.isInteger(hub.port) || hub.port <= 0) {
535
+ throw new Error(`invalid SEN TCP discovery hub: ${config.tcpHub}`);
536
+ }
537
+ const socket = net.createConnection({ host: hub.host, port: hub.port });
538
+ this.discoverySocket = socket;
539
+ socket.on('data', chunk => this.#onDiscoveryData(chunk));
540
+ socket.on('close', hadError => {
541
+ if (this.discoverySocket === socket) {
542
+ this.discoverySocket = undefined;
543
+ }
544
+ if (this.discoveryTimer) {
545
+ clearInterval(this.discoveryTimer);
546
+ this.discoveryTimer = undefined;
547
+ }
548
+ this.emit('discoveryClose', hadError);
549
+ });
550
+
551
+ await new Promise((resolve, reject) => {
552
+ const onError = error => {
553
+ socket.off('connect', onConnect);
554
+ reject(error);
555
+ };
556
+ const onConnect = () => {
557
+ socket.off('error', onError);
558
+ socket.on('error', error => this.emit('error', error));
559
+ this.#sendDiscoveryBeam();
560
+ const period = Math.max(100, Number(config.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS));
561
+ this.discoveryTimer = setInterval(() => this.#sendDiscoveryBeam(), period);
562
+ this.discoveryTimer.unref?.();
563
+ this.emit('discoveryConnect', hub);
564
+ resolve();
565
+ };
566
+ socket.once('error', onError);
567
+ socket.once('connect', onConnect);
568
+ });
569
+ }
570
+
571
+ async #startMulticastDiscovery(config) {
572
+ if (!this.listenEndpoint) {
573
+ return;
574
+ }
575
+ const group = config.group ?? DEFAULT_DISCOVERY_GROUP;
576
+ const port = config.port ?? config.discoveryPort ?? this.options.discoveryPort;
577
+ const bindAddress = config.bindAddress ?? (process.platform === 'win32' ? undefined : group);
578
+ const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
579
+ this.multicastDiscoverySocket = socket;
580
+ socket.on('message', (message, remote) => this.#onMulticastDiscoveryMessage(message, remote));
581
+ socket.on('error', error => this.emit('error', error));
582
+
583
+ await new Promise((resolve, reject) => {
584
+ const onError = error => {
585
+ socket.off('listening', onListening);
586
+ reject(error);
587
+ };
588
+ const onListening = () => {
589
+ socket.off('error', onError);
590
+ try {
591
+ const interfaces = multicastInterfaceCandidates(this.interfaceAddress);
592
+ if (interfaces.length) {
593
+ let joined = 0;
594
+ let firstError;
595
+ for (const interfaceAddress of interfaces) {
596
+ try {
597
+ socket.addMembership(group, interfaceAddress);
598
+ joined += 1;
599
+ } catch (error) {
600
+ firstError ??= error;
601
+ }
602
+ }
603
+ if (!joined) {
604
+ throw firstError ?? new Error(`could not join multicast group ${group}`);
605
+ }
606
+ } else {
607
+ socket.addMembership(group);
608
+ }
609
+ socket.setMulticastLoopback(true);
610
+ if (this.interfaceAddress) {
611
+ socket.setMulticastInterface(this.interfaceAddress);
612
+ }
613
+ this.#sendMulticastDiscoveryBeam();
614
+ const period = Math.max(100, Number(config.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS));
615
+ this.multicastDiscoveryTimer = setInterval(() => this.#sendMulticastDiscoveryBeam(), period);
616
+ this.multicastDiscoveryTimer.unref?.();
617
+ this.emit('multicastDiscoveryStart', { group, port });
618
+ resolve();
619
+ } catch (error) {
620
+ reject(error);
621
+ }
622
+ };
623
+ socket.once('error', onError);
624
+ socket.once('listening', onListening);
625
+ socket.bind(port, bindAddress);
626
+ });
627
+ }
628
+
629
+ #discoveryBeamBuffer({ padded = false } = {}) {
630
+ const beam = encodeSessionPresenceBeam({
631
+ protocolVersion: this.options.etherProtocolVersion,
632
+ info: this.processInfo,
633
+ beamPeriodNs: BigInt(Math.max(100, Number(this.options.beamPeriodMs ?? DEFAULT_BEAM_PERIOD_MS))) * 1_000_000n,
634
+ endpoints: [this.listenEndpoint]
635
+ });
636
+ return padded ? padDiscoveryBeam(beam) : beam;
637
+ }
638
+
639
+ #sendDiscoveryBeam() {
640
+ if (!this.discoverySocket || this.discoverySocket.destroyed || !this.listenEndpoint) {
641
+ return;
642
+ }
643
+ this.discoverySocket.write(this.#discoveryBeamBuffer({ padded: true }), error => {
644
+ if (error) {
645
+ this.emit('error', error);
646
+ }
299
647
  });
648
+ }
300
649
 
301
- this.socket = net.createConnection({ host: endpoint.host, port: endpoint.port });
302
- this.socket.on('data', chunk => this.#onTcpData(chunk));
303
- this.socket.on('close', hadError => {
304
- this.ready = false;
305
- this.emit('close', hadError);
650
+ #sendMulticastDiscoveryBeam() {
651
+ const socket = this.multicastDiscoverySocket;
652
+ if (!socket || !this.listenEndpoint) {
653
+ return;
654
+ }
655
+ const group = this.options.group ?? DEFAULT_DISCOVERY_GROUP;
656
+ const port = this.options.port ?? this.options.discoveryPort;
657
+ const beam = this.#discoveryBeamBuffer();
658
+ socket.send(beam, port, group, error => {
659
+ if (error) {
660
+ this.emit('error', error);
661
+ }
306
662
  });
663
+ }
664
+
665
+ #onDiscoveryData(chunk) {
666
+ this.discoveryReceiveBuffer = Buffer.concat([this.discoveryReceiveBuffer, chunk]);
667
+ while (this.discoveryReceiveBuffer.length >= TCP_DISCOVERY_BEAM_SIZE) {
668
+ const message = this.discoveryReceiveBuffer.subarray(0, TCP_DISCOVERY_BEAM_SIZE);
669
+ this.discoveryReceiveBuffer = this.discoveryReceiveBuffer.subarray(TCP_DISCOVERY_BEAM_SIZE);
670
+ this.#onDiscoveryBeam(message);
671
+ }
672
+ }
673
+
674
+ #onDiscoveryBeam(message) {
675
+ let beam;
676
+ try {
677
+ beam = decodeSessionPresenceBeam(message);
678
+ } catch (error) {
679
+ this.emit('decodeError', error, message);
680
+ return;
681
+ }
682
+ if (beam.info.sessionId !== this.processInfo.sessionId || isSameProcessInfo(beam.info, this.processInfo)) {
683
+ return;
684
+ }
685
+ const key = processKeyFromInfo(beam.info);
686
+ this.emit('beam', beam);
687
+ if (this.connectionsByProcessKey.has(key)) {
688
+ return;
689
+ }
690
+ const endpoint = beam.endpoints?.[0];
691
+ if (!endpoint) {
692
+ return;
693
+ }
694
+ this.connect({ ...beam, info: beam.info, endpoints: beam.endpoints }).catch(error => this.emit('warning', error));
695
+ }
696
+
697
+ #onMulticastDiscoveryMessage(message, remote) {
698
+ let beam;
699
+ try {
700
+ beam = decodeSessionPresenceBeam(message);
701
+ } catch (error) {
702
+ this.emit('decodeError', error, message, remote);
703
+ return;
704
+ }
705
+ if (beam.info.sessionId !== this.processInfo.sessionId || isSameProcessInfo(beam.info, this.processInfo)) {
706
+ return;
707
+ }
708
+ const key = processKeyFromInfo(beam.info);
709
+ this.emit('beam', beam);
710
+ if (this.connectionsByProcessKey.has(key)) {
711
+ return;
712
+ }
713
+ if (!beam.endpoints?.length) {
714
+ return;
715
+ }
716
+ this.connect({ ...beam, info: beam.info, endpoints: beam.endpoints }).catch(error => this.emit('warning', error));
717
+ }
718
+
719
+ /**
720
+ * Connect to one endpoint from a SessionPresenceBeam process entry.
721
+ *
722
+ * @param {{ endpoints?: Array<{ host: string, port: number }>, info?: object } | { host: string, port: number }} target
723
+ */
724
+ async connect(target) {
725
+ const endpoint = target.host ? target : target.endpoints?.[0];
726
+ if (!endpoint) {
727
+ throw new TypeError('SEN ether target must contain host/port or at least one endpoint');
728
+ }
729
+
730
+ if (!this.udpSocket) {
731
+ this.udpSocket = dgram.createSocket('udp4');
732
+ this.udpSocket.on('message', message => this.#onBusFrame(message));
733
+ this.udpSocket.on('error', error => this.emit('error', error));
734
+ await new Promise((resolve, reject) => {
735
+ const onError = error => {
736
+ this.udpSocket?.off('listening', onListening);
737
+ reject(error);
738
+ };
739
+ const onListening = () => {
740
+ this.udpSocket?.off('error', onError);
741
+ resolve();
742
+ };
743
+ this.udpSocket.once('error', onError);
744
+ this.udpSocket.once('listening', onListening);
745
+ this.udpSocket.bind(0);
746
+ });
747
+ }
748
+
749
+ const socket = net.createConnection({ host: endpoint.host, port: endpoint.port });
750
+ const connection = this.#registerSocket(socket, { target, incoming: false });
307
751
 
308
752
  await new Promise((resolve, reject) => {
309
753
  const onError = error => {
310
- this.socket?.off('connect', onConnect);
754
+ socket.off('connect', onConnect);
311
755
  reject(error);
312
756
  };
313
757
  const onConnect = () => {
314
- this.socket?.off('error', onError);
315
- this.socket?.on('error', error => this.emit('error', error));
316
- this.#configureTcpSocket();
758
+ socket.off('error', onError);
759
+ socket.on('error', error => this.emit('error', error));
760
+ this.#configureTcpSocket(socket);
317
761
  try {
318
- this.#sendHello();
762
+ this.#sendHello(connection);
319
763
  resolve();
320
764
  } catch (error) {
321
765
  reject(error);
322
766
  }
323
767
  };
324
- this.socket.once('error', onError);
325
- this.socket.once('connect', onConnect);
768
+ socket.once('error', onError);
769
+ socket.once('connect', onConnect);
326
770
  });
327
771
 
328
772
  return this;
329
773
  }
330
774
 
331
- #configureTcpSocket() {
775
+ #registerSocket(socket, metadata = {}) {
776
+ const connection = {
777
+ id: this.nextConnectionId++,
778
+ socket,
779
+ incoming: Boolean(metadata.incoming),
780
+ target: metadata.target,
781
+ receiveBuffer: Buffer.alloc(0),
782
+ remoteProcessInfo: metadata.target?.info,
783
+ ready: false
784
+ };
785
+ this.connections.set(connection.id, connection);
332
786
  if (!this.socket) {
787
+ this.socket = socket;
788
+ }
789
+ socket.on('data', chunk => this.#onTcpData(connection, chunk));
790
+ socket.on('close', hadError => {
791
+ this.#removeConnection(connection);
792
+ this.emit('connectionClose', { connection, hadError });
793
+ if (!this.connections.size) {
794
+ this.ready = false;
795
+ this.emit('close', hadError);
796
+ }
797
+ });
798
+ return connection;
799
+ }
800
+
801
+ #removeConnection(connection) {
802
+ this.connections.delete(connection.id);
803
+ if (connection.processKey) {
804
+ this.connectionsByProcessKey.delete(connection.processKey);
805
+ }
806
+ for (const [busId, participants] of this.remoteParticipantsByBusId) {
807
+ for (const [participantId, participant] of participants) {
808
+ if (participant.connection === connection) {
809
+ participants.delete(participantId);
810
+ }
811
+ }
812
+ if (!participants.size) {
813
+ this.remoteParticipantsByBusId.delete(busId);
814
+ }
815
+ }
816
+ for (const busState of this.buses.values()) {
817
+ for (const [key, interest] of busState.remoteInterests) {
818
+ if (interest.connection === connection) {
819
+ busState.remoteInterests.delete(key);
820
+ }
821
+ }
822
+ }
823
+ if (this.socket === connection.socket) {
824
+ this.socket = [...this.connections.values()][0]?.socket;
825
+ }
826
+ }
827
+
828
+ #configureTcpSocket(socket) {
829
+ if (!socket) {
333
830
  return;
334
831
  }
335
832
  if (this.options.socketKeepAlive !== false) {
336
- this.socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
833
+ socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
337
834
  }
338
835
  if (this.options.socketIdleTimeoutMs > 0) {
339
- this.socket.setTimeout(this.options.socketIdleTimeoutMs, () => {
836
+ socket.setTimeout(this.options.socketIdleTimeoutMs, () => {
340
837
  const error = new Error(`SEN ether TCP socket idle timeout after ${this.options.socketIdleTimeoutMs}ms`);
341
838
  error.code = 'SEN_TCP_IDLE_TIMEOUT';
342
- this.socket?.destroy(error);
839
+ socket.destroy(error);
343
840
  });
344
841
  }
345
842
  }
346
843
 
347
844
  async close() {
348
- const socket = this.socket;
845
+ const sockets = [...this.connections.values()].map(connection => connection.socket);
846
+ this.connections.clear();
847
+ this.connectionsByProcessKey.clear();
349
848
  this.socket = undefined;
350
849
  const udpSocket = this.udpSocket;
351
850
  this.udpSocket = undefined;
851
+ const server = this.server;
852
+ this.server = undefined;
853
+ const discoverySocket = this.discoverySocket;
854
+ this.discoverySocket = undefined;
855
+ const multicastDiscoverySocket = this.multicastDiscoverySocket;
856
+ this.multicastDiscoverySocket = undefined;
857
+ if (this.discoveryTimer) {
858
+ clearInterval(this.discoveryTimer);
859
+ this.discoveryTimer = undefined;
860
+ }
861
+ if (this.multicastDiscoveryTimer) {
862
+ clearInterval(this.multicastDiscoveryTimer);
863
+ this.multicastDiscoveryTimer = undefined;
864
+ }
352
865
 
353
866
  const closing = [];
354
- if (socket && !socket.destroyed) {
355
- closing.push(new Promise(resolve => {
356
- socket.once('close', resolve);
357
- socket.destroy();
867
+ for (const socket of sockets) {
868
+ if (socket && !socket.destroyed) {
869
+ closing.push(withCloseTimeout(resolve => {
870
+ socket.once('close', resolve);
871
+ socket.destroy();
872
+ }));
873
+ }
874
+ }
875
+ if (server) {
876
+ server.closeAllConnections?.();
877
+ closing.push(withCloseTimeout(resolve => {
878
+ server.close(resolve);
879
+ }));
880
+ }
881
+ if (discoverySocket && !discoverySocket.destroyed) {
882
+ closing.push(withCloseTimeout(resolve => {
883
+ discoverySocket.once('close', resolve);
884
+ discoverySocket.destroy();
885
+ }));
886
+ }
887
+ if (multicastDiscoverySocket) {
888
+ closing.push(withCloseTimeout(resolve => {
889
+ multicastDiscoverySocket.close(resolve);
358
890
  }));
359
891
  }
360
892
  if (udpSocket) {
361
- closing.push(new Promise(resolve => {
893
+ closing.push(withCloseTimeout(resolve => {
362
894
  udpSocket.close(resolve);
363
895
  }));
364
896
  }
365
897
  for (const busState of this.buses.values()) {
366
898
  if (busState.multicastSocket) {
367
- closing.push(new Promise(resolve => {
899
+ closing.push(withCloseTimeout(resolve => {
368
900
  busState.multicastSocket.close(resolve);
369
901
  }));
370
902
  }
@@ -381,11 +913,21 @@ export class EtherClient extends EventEmitter {
381
913
  * @param {{ participantId?: number }} [options]
382
914
  */
383
915
  async joinBus(busName, options = {}) {
384
- if (!this.socket) {
385
- throw new Error('EtherClient is not connected');
916
+ if (!this.connections.size && !this.server) {
917
+ throw new Error('EtherClient is not connected or started');
386
918
  }
387
919
 
388
920
  const busId = crc32(busName);
921
+ const existing = this.buses.get(busId);
922
+ if (existing) {
923
+ return {
924
+ busName: existing.busName,
925
+ busId: existing.busId,
926
+ participantId: existing.participantId,
927
+ multicastGroup: existing.multicastGroup,
928
+ multicastPort: this.options.busMulticastPort
929
+ };
930
+ }
389
931
  const participantId = options.participantId ?? randomUInt32();
390
932
  const bus = {
391
933
  busName,
@@ -393,6 +935,10 @@ export class EtherClient extends EventEmitter {
393
935
  participantId,
394
936
  readyRemoteParticipants: new Set(),
395
937
  interests: new Map(),
938
+ remoteInterests: new Map(),
939
+ publishedObjects: new Map(),
940
+ localTypeRegistry: new Map(),
941
+ localTypeResponsesByHash: new Map(),
396
942
  multicastSocket: undefined,
397
943
  multicastGroup: undefined
398
944
  };
@@ -407,7 +953,7 @@ export class EtherClient extends EventEmitter {
407
953
  throw error;
408
954
  }
409
955
 
410
- this.#sendControlPayload(encodeEtherControlMessage({
956
+ this.#sendControlPayloadToAll(encodeEtherControlMessage({
411
957
  type: 'BusJoined',
412
958
  value: {
413
959
  participantId,
@@ -416,6 +962,13 @@ export class EtherClient extends EventEmitter {
416
962
  }
417
963
  }));
418
964
 
965
+ for (const participant of this.#remoteParticipantsForBus(bus.busId)) {
966
+ this.#sendBusControlToConnection(bus, participant.connection, {
967
+ type: 'RemoteParticipantReady',
968
+ value: { id: participant.id }
969
+ });
970
+ }
971
+
419
972
  this.emit('busJoinedLocal', {
420
973
  busName,
421
974
  busId,
@@ -445,7 +998,7 @@ export class EtherClient extends EventEmitter {
445
998
  startInterest(bus, query, options = {}) {
446
999
  const busState = this.#getBus(bus);
447
1000
  const id = options.id ?? crc32(query);
448
- this.#sendBusControl(busState, busState.participantId, {
1001
+ this.#sendBusControlToRemoteParticipants(busState, {
449
1002
  type: 'InterestStarted',
450
1003
  value: { query, id }
451
1004
  });
@@ -455,8 +1008,19 @@ export class EtherClient extends EventEmitter {
455
1008
  }
456
1009
 
457
1010
  #restartInterestForRemote(busState) {
1011
+ for (const participant of this.#remoteParticipantsForBus(busState.busId)) {
1012
+ for (const interest of busState.interests.values()) {
1013
+ this.#sendBusControlToConnection(busState, participant.connection, {
1014
+ type: 'InterestStarted',
1015
+ value: { query: interest.query, id: interest.id }
1016
+ });
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ #restartInterestForConnection(busState, connection) {
458
1022
  for (const interest of busState.interests.values()) {
459
- this.#sendBusControl(busState, busState.participantId, {
1023
+ this.#sendBusControlToConnection(busState, connection, {
460
1024
  type: 'InterestStarted',
461
1025
  value: { query: interest.query, id: interest.id }
462
1026
  });
@@ -472,7 +1036,7 @@ export class EtherClient extends EventEmitter {
472
1036
  stopInterest(bus, id) {
473
1037
  const busState = this.#getBus(bus);
474
1038
  busState.interests.delete(id);
475
- this.#sendBusControl(busState, busState.participantId, {
1039
+ this.#sendBusControlToRemoteParticipants(busState, {
476
1040
  type: 'InterestStopped',
477
1041
  value: { id }
478
1042
  });
@@ -491,7 +1055,7 @@ export class EtherClient extends EventEmitter {
491
1055
  return { busName: busState.busName, busId: busState.busId, requests };
492
1056
  }
493
1057
 
494
- this.#sendBusControl(busState, busState.participantId, {
1058
+ this.#sendBusControlToRemoteParticipants(busState, {
495
1059
  type: 'TypesInfoRequest',
496
1060
  value: {
497
1061
  ownerId: busState.participantId,
@@ -521,7 +1085,7 @@ export class EtherClient extends EventEmitter {
521
1085
  return { busName: busState.busName, busId: busState.busId, requests: normalized };
522
1086
  }
523
1087
 
524
- this.#sendBusControl(busState, busState.participantId, {
1088
+ this.#sendBusControlToRemoteParticipants(busState, {
525
1089
  type: 'ObjectsStateRequest',
526
1090
  value: {
527
1091
  ownerId: busState.participantId,
@@ -532,6 +1096,75 @@ export class EtherClient extends EventEmitter {
532
1096
  return { busName: busState.busName, busId: busState.busId, requests: normalized };
533
1097
  }
534
1098
 
1099
+ /**
1100
+ * Publish local JavaScript objects on a joined SEN bus.
1101
+ *
1102
+ * Objects need at least `{ name, className, properties }`. A `spec` can be
1103
+ * supplied for exact SEN typing; otherwise a simple ClassTypeSpec is inferred
1104
+ * from the current property values.
1105
+ *
1106
+ * @param {string | number} bus Bus name or bus id.
1107
+ * @param {object|object[]} objects
1108
+ * @param {{ types?: Map<string, object>|Record<string, object>|object[] }} [options]
1109
+ */
1110
+ publishObjects(bus, objects, options = {}) {
1111
+ const busState = this.#getBus(bus);
1112
+ const list = Array.isArray(objects) ? objects : [objects];
1113
+ const externalTypes = normalizeTypeDefinitions(options.types);
1114
+ for (const type of externalTypes) {
1115
+ this.#registerLocalType(busState, type);
1116
+ }
1117
+
1118
+ const published = [];
1119
+ for (const item of list) {
1120
+ const localObject = buildLocalObject(item, busState.localTypeRegistry);
1121
+ busState.publishedObjects.set(localObject.id, localObject);
1122
+ this.#registerLocalType(busState, localObject.spec, localObject.typeHash);
1123
+ published.push(localObject);
1124
+ }
1125
+
1126
+ if (published.length) {
1127
+ this.#publishObjectsToRemoteInterests(busState, published);
1128
+ }
1129
+
1130
+ this.emit('objectsPublishedLocal', {
1131
+ busName: busState.busName,
1132
+ busId: busState.busId,
1133
+ objects: published
1134
+ });
1135
+ return published;
1136
+ }
1137
+
1138
+ /**
1139
+ * Remove previously published local objects from a joined bus.
1140
+ *
1141
+ * @param {string | number} bus Bus name or bus id.
1142
+ * @param {Array<string|number>|string|number} objects Object ids or names.
1143
+ */
1144
+ removePublishedObjects(bus, objects) {
1145
+ const busState = this.#getBus(bus);
1146
+ const selectors = Array.isArray(objects) ? objects : [objects];
1147
+ const removed = [];
1148
+ for (const selector of selectors) {
1149
+ const id = typeof selector === 'number'
1150
+ ? selector >>> 0
1151
+ : [...busState.publishedObjects.values()].find(object => object.name === selector)?.id;
1152
+ if (id === undefined) continue;
1153
+ if (busState.publishedObjects.delete(id)) removed.push(id);
1154
+ }
1155
+
1156
+ if (removed.length) {
1157
+ this.#removeObjectsFromRemoteInterests(busState, removed);
1158
+ }
1159
+
1160
+ this.emit('objectsRemovedLocal', {
1161
+ busName: busState.busName,
1162
+ busId: busState.busId,
1163
+ objectIds: removed
1164
+ });
1165
+ return removed;
1166
+ }
1167
+
535
1168
  /**
536
1169
  * Send a runtime method call to a remote participant on a joined bus.
537
1170
  *
@@ -546,6 +1179,7 @@ export class EtherClient extends EventEmitter {
546
1179
  */
547
1180
  sendRuntimeMethodCall(bus, call) {
548
1181
  const busState = this.#getBus(bus);
1182
+ const remote = this.#remoteParticipantForBus(busState.busId, call.to);
549
1183
  const message = encodeRuntimeMethodCall({
550
1184
  ownerId: busState.participantId,
551
1185
  objectId: call.objectId,
@@ -554,7 +1188,7 @@ export class EtherClient extends EventEmitter {
554
1188
  confirmed: call.confirmed,
555
1189
  argumentsBuffer: call.argumentsBuffer
556
1190
  });
557
- this.#sendBusMessage(busState, call.to, message);
1191
+ this.#sendBusMessageToConnection(busState, remote?.connection, message);
558
1192
  this.emit('runtimeMethodCallSent', {
559
1193
  busName: busState.busName,
560
1194
  busId: busState.busId,
@@ -577,7 +1211,7 @@ export class EtherClient extends EventEmitter {
577
1211
  this.stopInterest(busState.busId, id);
578
1212
  }
579
1213
 
580
- this.#sendControlPayload(encodeEtherControlMessage({
1214
+ this.#sendControlPayloadToAll(encodeEtherControlMessage({
581
1215
  type: 'BusLeft',
582
1216
  value: {
583
1217
  participantId: busState.participantId,
@@ -597,7 +1231,7 @@ export class EtherClient extends EventEmitter {
597
1231
  });
598
1232
  }
599
1233
 
600
- #sendHello() {
1234
+ #sendHello(connection) {
601
1235
  const udpPort = this.udpSocket?.address()?.port;
602
1236
  const payload = encodeEtherControlMessage({
603
1237
  type: 'Hello',
@@ -610,15 +1244,15 @@ export class EtherClient extends EventEmitter {
610
1244
  }
611
1245
  }
612
1246
  });
613
- this.#sendControlPayload(payload);
1247
+ this.#sendControlPayloadToConnection(connection, payload);
614
1248
  }
615
1249
 
616
- #sendReady() {
617
- this.#sendControlPayload(encodeEtherControlMessage({ type: 'Ready' }));
1250
+ #sendReady(connection) {
1251
+ this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({ type: 'Ready' }));
618
1252
  }
619
1253
 
620
- #sendControlPayload(payload) {
621
- const socket = this.#writableSocket();
1254
+ #sendControlPayloadToConnection(connection, payload) {
1255
+ const socket = this.#writableConnectionSocket(connection);
622
1256
  socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.controlMessage, payload), error => {
623
1257
  if (error) {
624
1258
  this.emit('error', error);
@@ -626,88 +1260,158 @@ export class EtherClient extends EventEmitter {
626
1260
  });
627
1261
  }
628
1262
 
629
- #onTcpData(chunk) {
630
- this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
1263
+ #sendControlPayloadToAll(payload) {
1264
+ for (const connection of this.connections.values()) {
1265
+ this.#sendControlPayloadToConnection(connection, payload);
1266
+ }
1267
+ }
1268
+
1269
+ #onTcpData(connection, chunk) {
1270
+ connection.receiveBuffer = Buffer.concat([connection.receiveBuffer, chunk]);
631
1271
 
632
- while (this.receiveBuffer.length >= 5) {
633
- const header = decodeProcessTcpHeader(this.receiveBuffer);
1272
+ while (connection.receiveBuffer.length >= 5) {
1273
+ const header = decodeProcessTcpHeader(connection.receiveBuffer);
634
1274
  const frameSize = 5 + header.payloadSize;
635
- if (this.receiveBuffer.length < frameSize) {
1275
+ if (connection.receiveBuffer.length < frameSize) {
636
1276
  return;
637
1277
  }
638
1278
 
639
- const payload = this.receiveBuffer.subarray(5, frameSize);
640
- this.receiveBuffer = this.receiveBuffer.subarray(frameSize);
641
- this.#onFrame(header.category, payload);
1279
+ const payload = connection.receiveBuffer.subarray(5, frameSize);
1280
+ connection.receiveBuffer = connection.receiveBuffer.subarray(frameSize);
1281
+ this.#onFrame(connection, header.category, payload);
642
1282
  }
643
1283
  }
644
1284
 
645
- #onFrame(category, payload) {
1285
+ #onFrame(connection, category, payload) {
646
1286
  if (category === PROCESS_MESSAGE_CATEGORY.controlMessage) {
647
1287
  const message = decodeEtherControlMessage(payload);
648
- this.emit('controlMessage', message);
649
- this.#onControlMessage(message);
1288
+ this.emit('controlMessage', message, connection);
1289
+ this.#onControlMessage(connection, message);
650
1290
  return;
651
1291
  }
652
1292
 
653
1293
  if (category === PROCESS_MESSAGE_CATEGORY.busMessage) {
654
- this.#onBusFrame(payload);
1294
+ this.#onBusFrame(payload, connection);
655
1295
  return;
656
1296
  }
657
1297
 
658
1298
  this.emit('error', new RangeError(`unknown SEN process frame category: ${category}`));
659
1299
  }
660
1300
 
661
- #onControlMessage(message) {
1301
+ #onControlMessage(connection, message) {
662
1302
  switch (message.type) {
663
1303
  case 'Hello':
664
- this.#onHello(message.value);
1304
+ this.#onHello(connection, message.value);
665
1305
  break;
666
1306
  case 'Ready':
1307
+ connection.ready = true;
667
1308
  this.ready = true;
668
- this.emit('ready', this.remoteProcessInfo);
1309
+ this.emit('ready', connection.remoteProcessInfo);
1310
+ this.emit('connectionReady', { connection, remoteProcessInfo: connection.remoteProcessInfo });
669
1311
  break;
670
1312
  case 'BusJoined':
671
- this.emit('busJoined', message.value);
1313
+ this.#onRemoteBusJoined(connection, message.value);
672
1314
  break;
673
1315
  case 'BusLeft':
674
- this.emit('busLeft', message.value);
1316
+ this.#onRemoteBusLeft(connection, message.value);
675
1317
  break;
676
1318
  default:
677
1319
  this.emit('error', new RangeError(`unknown SEN ether control message: ${message.type}`));
678
1320
  }
679
1321
  }
680
1322
 
681
- #onHello(hello) {
1323
+ #onHello(connection, hello) {
682
1324
  try {
683
1325
  validateRemoteHello(hello, this.options, this.processInfo);
684
1326
  } catch (error) {
685
- this.socket?.destroy(error);
1327
+ connection.socket?.destroy(error);
686
1328
  return;
687
1329
  }
688
1330
 
689
- this.remoteProcessInfo = hello.info;
1331
+ connection.remoteProcessInfo = hello.info;
1332
+ connection.processKey = processKeyFromInfo(hello.info);
1333
+ this.connectionsByProcessKey.set(connection.processKey, connection);
1334
+ this.remoteProcessInfo ??= hello.info;
690
1335
  this.emit('remoteProcess', hello);
691
1336
  try {
692
- this.#sendReady();
1337
+ this.#sendReady(connection);
1338
+ this.#announceLocalBusesToConnection(connection);
693
1339
  } catch (error) {
694
1340
  this.emit('error', error);
695
1341
  }
696
1342
  }
697
1343
 
698
- #onBusFrame(payload) {
1344
+ #announceLocalBusesToConnection(connection) {
1345
+ for (const busState of this.buses.values()) {
1346
+ this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({
1347
+ type: 'BusJoined',
1348
+ value: {
1349
+ participantId: busState.participantId,
1350
+ busId: busState.busId,
1351
+ busName: busState.busName
1352
+ }
1353
+ }));
1354
+ }
1355
+ }
1356
+
1357
+ #onRemoteBusJoined(connection, value) {
1358
+ const participant = {
1359
+ id: value.participantId >>> 0,
1360
+ busId: value.busId >>> 0,
1361
+ busName: value.busName,
1362
+ connection
1363
+ };
1364
+ let participants = this.remoteParticipantsByBusId.get(participant.busId);
1365
+ if (!participants) {
1366
+ participants = new Map();
1367
+ this.remoteParticipantsByBusId.set(participant.busId, participants);
1368
+ }
1369
+ participants.set(participant.id, participant);
1370
+ this.emit('busJoined', { ...value, connection });
1371
+
1372
+ const busState = this.buses.get(participant.busId);
1373
+ if (busState) {
1374
+ this.#sendBusControlToConnection(busState, connection, {
1375
+ type: 'RemoteParticipantReady',
1376
+ value: { id: participant.id }
1377
+ });
1378
+ this.#restartInterestForConnection(busState, connection);
1379
+ this.#publishObjectsToRemoteInterests(busState, [...busState.publishedObjects.values()]);
1380
+ }
1381
+ }
1382
+
1383
+ #onRemoteBusLeft(connection, value) {
1384
+ const busId = value.busId >>> 0;
1385
+ const participantId = value.participantId >>> 0;
1386
+ const participants = this.remoteParticipantsByBusId.get(busId);
1387
+ participants?.delete(participantId);
1388
+ if (participants && !participants.size) {
1389
+ this.remoteParticipantsByBusId.delete(busId);
1390
+ }
1391
+ const busState = this.buses.get(busId);
1392
+ if (busState) {
1393
+ for (const [key, interest] of busState.remoteInterests) {
1394
+ if (interest.connection === connection && interest.participantId === participantId) {
1395
+ busState.remoteInterests.delete(key);
1396
+ }
1397
+ }
1398
+ }
1399
+ this.emit('busLeft', { ...value, connection });
1400
+ }
1401
+
1402
+ #onBusFrame(payload, connection = undefined) {
699
1403
  const frame = decodeConfirmedBusFrame(payload);
700
1404
  const busMessage = decodeBusMessage(frame.message);
701
- this.emit('busFrame', { ...frame, busMessage });
1405
+ this.emit('busFrame', { ...frame, busMessage, connection });
702
1406
 
703
1407
  if (busMessage.categoryName !== 'controlMessage') {
704
- this.emit(busMessage.categoryName, { ...frame, ...busMessage });
1408
+ this.emit(busMessage.categoryName, { ...frame, ...busMessage, connection });
705
1409
  return;
706
1410
  }
707
1411
 
708
1412
  const busState = this.buses.get(frame.busId);
709
1413
  const control = busMessage.control;
710
- this.emit('busControlMessage', { ...frame, control });
1414
+ this.emit('busControlMessage', { ...frame, control, connection });
711
1415
 
712
1416
  if (!busState) {
713
1417
  return;
@@ -715,22 +1419,34 @@ export class EtherClient extends EventEmitter {
715
1419
 
716
1420
  switch (control.type) {
717
1421
  case 'RemoteParticipantReady':
718
- this.#onRemoteParticipantReady(busState, frame, control.value);
1422
+ this.#onRemoteParticipantReady(busState, frame, control.value, connection);
1423
+ break;
1424
+ case 'InterestStarted':
1425
+ this.#onRemoteInterestStarted(busState, frame, control.value, connection);
1426
+ break;
1427
+ case 'InterestStopped':
1428
+ this.#onRemoteInterestStopped(busState, frame, control.value, connection);
719
1429
  break;
720
1430
  case 'ObjectsPublished':
721
- this.emit('objectsPublished', { bus: busState, ...control.value });
1431
+ this.emit('objectsPublished', { bus: busState, connection, ...control.value });
722
1432
  break;
723
1433
  case 'ObjectsRemoved':
724
- this.emit('objectsRemoved', { bus: busState, ...control.value });
1434
+ this.emit('objectsRemoved', { bus: busState, connection, ...control.value });
725
1435
  break;
726
1436
  case 'ObjectsStateResponse':
727
- this.emit('objectsStateResponse', { bus: busState, ...control.value });
1437
+ this.emit('objectsStateResponse', { bus: busState, connection, ...control.value });
728
1438
  break;
729
1439
  case 'TypesInfoResponse':
730
- this.emit('typesInfoResponse', { bus: busState, ...control.value });
1440
+ this.emit('typesInfoResponse', { bus: busState, connection, ...control.value });
731
1441
  break;
732
1442
  case 'TypesInfoRejection':
733
- this.emit('typesInfoRejection', { bus: busState, ...control.value });
1443
+ this.emit('typesInfoRejection', { bus: busState, connection, ...control.value });
1444
+ break;
1445
+ case 'ObjectsStateRequest':
1446
+ this.#onObjectsStateRequest(busState, frame, control.value, connection);
1447
+ break;
1448
+ case 'TypesInfoRequest':
1449
+ this.#onTypesInfoRequest(busState, frame, control.value, connection);
734
1450
  break;
735
1451
  default:
736
1452
  break;
@@ -807,7 +1523,7 @@ export class EtherClient extends EventEmitter {
807
1523
  });
808
1524
  }
809
1525
 
810
- #onRemoteParticipantReady(busState, frame, value) {
1526
+ #onRemoteParticipantReady(busState, frame, value, connection) {
811
1527
  if (value.id !== busState.participantId) {
812
1528
  return;
813
1529
  }
@@ -815,29 +1531,210 @@ export class EtherClient extends EventEmitter {
815
1531
  const remoteParticipantId = frame.to >>> 0;
816
1532
  if (!busState.readyRemoteParticipants.has(remoteParticipantId)) {
817
1533
  busState.readyRemoteParticipants.add(remoteParticipantId);
818
- this.#sendBusControl(busState, busState.participantId, {
1534
+ this.#sendBusControlToConnection(busState, connection, {
819
1535
  type: 'RemoteParticipantReady',
820
1536
  value: { id: remoteParticipantId }
821
1537
  });
822
- this.#restartInterestForRemote(busState);
1538
+ this.#restartInterestForConnection(busState, connection);
823
1539
  this.emit('busParticipantReady', {
824
1540
  busName: busState.busName,
825
1541
  busId: busState.busId,
826
1542
  participantId: busState.participantId,
827
- remoteParticipantId
1543
+ remoteParticipantId,
1544
+ connection
1545
+ });
1546
+ }
1547
+ }
1548
+
1549
+ #onRemoteInterestStarted(busState, frame, value, connection) {
1550
+ const remoteParticipantId = frame.to >>> 0;
1551
+ const id = value.id >>> 0;
1552
+ const key = `${connection?.id ?? 0}:${remoteParticipantId}:${id}`;
1553
+ busState.remoteInterests.set(key, {
1554
+ participantId: remoteParticipantId,
1555
+ connection,
1556
+ id,
1557
+ query: value.query
1558
+ });
1559
+ this.emit('remoteInterestStarted', {
1560
+ busName: busState.busName,
1561
+ busId: busState.busId,
1562
+ participantId: remoteParticipantId,
1563
+ connection,
1564
+ id,
1565
+ query: value.query
1566
+ });
1567
+ this.#publishObjectsToRemoteInterests(busState, [...busState.publishedObjects.values()], [key]);
1568
+ }
1569
+
1570
+ #onRemoteInterestStopped(busState, frame, value, connection) {
1571
+ const remoteParticipantId = frame.to >>> 0;
1572
+ const id = value.id >>> 0;
1573
+ busState.remoteInterests.delete(`${connection?.id ?? 0}:${remoteParticipantId}:${id}`);
1574
+ this.emit('remoteInterestStopped', {
1575
+ busName: busState.busName,
1576
+ busId: busState.busId,
1577
+ participantId: remoteParticipantId,
1578
+ connection,
1579
+ id
1580
+ });
1581
+ }
1582
+
1583
+ #onObjectsStateRequest(busState, frame, value, connection) {
1584
+ const remoteParticipantId = frame.to >>> 0;
1585
+ const responses = [];
1586
+ for (const request of value.requests ?? []) {
1587
+ const objectStates = [];
1588
+ for (const objectId of request.objectIds ?? []) {
1589
+ const object = busState.publishedObjects.get(objectId >>> 0);
1590
+ if (!object) continue;
1591
+ objectStates.push({
1592
+ id: object.id,
1593
+ timestamp: object.timestamp,
1594
+ state: object.stateBuffer
1595
+ });
1596
+ }
1597
+ if (objectStates.length) {
1598
+ responses.push({ interestId: request.interestId, objectStates });
1599
+ }
1600
+ }
1601
+ if (!responses.length) return;
1602
+ this.#sendBusControlToConnection(busState, connection, {
1603
+ type: 'ObjectsStateResponse',
1604
+ value: {
1605
+ ownerId: busState.participantId,
1606
+ responses
1607
+ }
1608
+ });
1609
+ }
1610
+
1611
+ #onTypesInfoRequest(busState, frame, value, connection) {
1612
+ const remoteParticipantId = frame.to >>> 0;
1613
+ const types = [];
1614
+ const rejections = [];
1615
+ for (const request of value.requests ?? []) {
1616
+ const type = busState.localTypeResponsesByHash.get(request >>> 0);
1617
+ if (type) {
1618
+ types.push(type);
1619
+ } else {
1620
+ rejections.push(String(request));
1621
+ }
1622
+ }
1623
+ if (types.length) {
1624
+ this.#sendBusControlToConnection(busState, connection, {
1625
+ type: 'TypesInfoResponse',
1626
+ value: {
1627
+ ownerId: busState.participantId,
1628
+ types
1629
+ }
1630
+ });
1631
+ }
1632
+ if (rejections.length) {
1633
+ this.#sendBusControlToConnection(busState, connection, {
1634
+ type: 'TypesInfoRejection',
1635
+ value: {
1636
+ ownerId: busState.participantId,
1637
+ rejections
1638
+ }
828
1639
  });
829
1640
  }
830
1641
  }
831
1642
 
832
- #sendBusControl(busState, to, message) {
1643
+ #registerLocalType(busState, spec, hash = crc32(spec?.qualifiedName ?? spec?.name ?? '')) {
1644
+ if (!spec?.qualifiedName) return;
1645
+ busState.localTypeRegistry.set(spec.qualifiedName, spec);
1646
+ const response = spec.data?.type === 'ClassTypeSpec'
1647
+ ? {
1648
+ type: 'ClassSpecResponse',
1649
+ classHash: hash >>> 0,
1650
+ spec,
1651
+ dependentTypes: []
1652
+ }
1653
+ : {
1654
+ type: 'NonClassSpecResponse',
1655
+ spec
1656
+ };
1657
+ busState.localTypeResponsesByHash.set((hash >>> 0), response);
1658
+ }
1659
+
1660
+ #publishObjectsToRemoteInterests(busState, objects, keys = undefined) {
1661
+ if (!objects.length) return;
1662
+ const targets = (keys ?? [...busState.remoteInterests.keys()])
1663
+ .map(key => busState.remoteInterests.get(key))
1664
+ .filter(Boolean);
1665
+ if (!targets.length) return;
1666
+
1667
+ const byConnection = new Map();
1668
+ for (const interest of targets) {
1669
+ const connection = interest.connection;
1670
+ if (!connection) continue;
1671
+ const list = byConnection.get(connection) ?? [];
1672
+ list.push({
1673
+ interestId: interest.id,
1674
+ objects: objects.map(object => ({
1675
+ className: object.className,
1676
+ typeHash: object.typeHash,
1677
+ name: object.name,
1678
+ id: object.id,
1679
+ state: object.stateBuffer,
1680
+ time: object.timestamp
1681
+ }))
1682
+ });
1683
+ byConnection.set(connection, list);
1684
+ }
1685
+
1686
+ for (const [connection, discoveries] of byConnection) {
1687
+ this.#sendBusControlToConnection(busState, connection, {
1688
+ type: 'ObjectsPublished',
1689
+ value: {
1690
+ ownerId: busState.participantId,
1691
+ discoveries
1692
+ }
1693
+ });
1694
+ }
1695
+ }
1696
+
1697
+ #removeObjectsFromRemoteInterests(busState, objectIds) {
1698
+ const targets = [...busState.remoteInterests.values()];
1699
+ if (!targets.length || !objectIds.length) return;
1700
+ const byConnection = new Map();
1701
+ for (const interest of targets) {
1702
+ const connection = interest.connection;
1703
+ if (!connection) continue;
1704
+ const removals = byConnection.get(connection) ?? [];
1705
+ removals.push({ interestId: interest.id, ids: objectIds });
1706
+ byConnection.set(connection, removals);
1707
+ }
1708
+ for (const [connection, removals] of byConnection) {
1709
+ this.#sendBusControlToConnection(busState, connection, {
1710
+ type: 'ObjectsRemoved',
1711
+ value: { removals }
1712
+ });
1713
+ }
1714
+ }
1715
+
1716
+ #sendBusControlToRemoteParticipants(busState, message) {
1717
+ const connections = new Set(this.#remoteParticipantsForBus(busState.busId).map(participant => participant.connection));
1718
+ for (const connection of connections) {
1719
+ this.#sendBusControlToConnection(busState, connection, message);
1720
+ }
1721
+ }
1722
+
1723
+ #sendBusControlToConnection(busState, connection, message) {
833
1724
  const busPayload = encodeBusControlMessage(message);
834
- this.#sendBusMessage(busState, to, busPayload);
1725
+ this.#sendBusMessageToConnection(busState, connection, busPayload);
835
1726
  }
836
1727
 
837
- #sendBusMessage(busState, to, busPayload) {
838
- const socket = this.#writableSocket();
1728
+ #sendBusMessageToConnection(busState, connection, busPayload) {
1729
+ if (!connection) {
1730
+ for (const participant of this.#remoteParticipantsForBus(busState.busId)) {
1731
+ this.#sendBusMessageToConnection(busState, participant.connection, busPayload);
1732
+ }
1733
+ return;
1734
+ }
1735
+ const socket = this.#writableConnectionSocket(connection);
839
1736
  const processBusPayload = encodeConfirmedBusFrame({
840
- to,
1737
+ to: busState.participantId,
841
1738
  busId: busState.busId,
842
1739
  message: busPayload
843
1740
  });
@@ -848,14 +1745,23 @@ export class EtherClient extends EventEmitter {
848
1745
  });
849
1746
  }
850
1747
 
851
- #writableSocket() {
852
- if (!this.socket || this.socket.destroyed || !this.socket.writable) {
1748
+ #writableConnectionSocket(connection) {
1749
+ const socket = connection?.socket;
1750
+ if (!socket || socket.destroyed || !socket.writable) {
853
1751
  const error = new Error('SEN ether TCP socket is not writable');
854
1752
  error.code = 'SEN_TCP_NOT_WRITABLE';
855
1753
  this.emit('error', error);
856
1754
  throw error;
857
1755
  }
858
- return this.socket;
1756
+ return socket;
1757
+ }
1758
+
1759
+ #remoteParticipantsForBus(busId) {
1760
+ return [...(this.remoteParticipantsByBusId.get(busId >>> 0)?.values() ?? [])];
1761
+ }
1762
+
1763
+ #remoteParticipantForBus(busId, participantId) {
1764
+ return this.remoteParticipantsByBusId.get(busId >>> 0)?.get(participantId >>> 0);
859
1765
  }
860
1766
 
861
1767
  #getBus(bus) {