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/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
- * Connect to one endpoint from a SessionPresenceBeam process entry.
477
+ * Start this JS process as an active Ether node.
275
478
  *
276
- * @param {{ endpoints?: Array<{ host: string, port: number }>, info?: object } | { host: string, port: number }} target
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 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');
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
- this.udpSocket?.off('listening', onListening);
510
+ server.off('listening', onListening);
290
511
  reject(error);
291
512
  };
292
513
  const onListening = () => {
293
- this.udpSocket?.off('error', onError);
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
- this.udpSocket.once('error', onError);
297
- this.udpSocket.once('listening', onListening);
298
- this.udpSocket.bind(0);
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
- 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);
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
- this.socket?.off('connect', onConnect);
755
+ socket.off('connect', onConnect);
311
756
  reject(error);
312
757
  };
313
758
  const onConnect = () => {
314
- this.socket?.off('error', onError);
315
- this.socket?.on('error', error => this.emit('error', error));
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
- this.socket.once('error', onError);
325
- this.socket.once('connect', onConnect);
769
+ socket.once('error', onError);
770
+ socket.once('connect', onConnect);
326
771
  });
327
772
 
328
773
  return this;
329
774
  }
330
775
 
331
- #configureTcpSocket() {
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
- this.socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
834
+ socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
337
835
  }
338
836
  if (this.options.socketIdleTimeoutMs > 0) {
339
- this.socket.setTimeout(this.options.socketIdleTimeoutMs, () => {
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
- this.socket?.destroy(error);
840
+ socket.destroy(error);
343
841
  });
344
842
  }
345
843
  }
346
844
 
347
845
  async close() {
348
- const socket = this.socket;
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
- if (socket && !socket.destroyed) {
355
- closing.push(new Promise(resolve => {
356
- socket.once('close', resolve);
357
- socket.destroy();
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(new Promise(resolve => {
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(new Promise(resolve => {
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.socket) {
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.#sendControlPayload(encodeEtherControlMessage({
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 ?? crc32(query);
448
- this.#sendBusControl(busState, busState.participantId, {
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.#sendBusControl(busState, busState.participantId, {
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.#sendBusControl(busState, busState.participantId, {
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.#sendBusControl(busState, busState.participantId, {
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.#sendBusControl(busState, busState.participantId, {
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.#sendBusMessage(busState, call.to, message);
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.#sendControlPayload(encodeEtherControlMessage({
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.#sendControlPayload(payload);
1260
+ this.#sendControlPayloadToConnection(connection, payload);
614
1261
  }
615
1262
 
616
- #sendReady() {
617
- this.#sendControlPayload(encodeEtherControlMessage({ type: 'Ready' }));
1263
+ #sendReady(connection) {
1264
+ this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({ type: 'Ready' }));
618
1265
  }
619
1266
 
620
- #sendControlPayload(payload) {
621
- const socket = this.#writableSocket();
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
- #onTcpData(chunk) {
630
- this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
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 (this.receiveBuffer.length >= 5) {
633
- const header = decodeProcessTcpHeader(this.receiveBuffer);
1285
+ while (connection.receiveBuffer.length >= 5) {
1286
+ const header = decodeProcessTcpHeader(connection.receiveBuffer);
634
1287
  const frameSize = 5 + header.payloadSize;
635
- if (this.receiveBuffer.length < frameSize) {
1288
+ if (connection.receiveBuffer.length < frameSize) {
636
1289
  return;
637
1290
  }
638
1291
 
639
- const payload = this.receiveBuffer.subarray(5, frameSize);
640
- this.receiveBuffer = this.receiveBuffer.subarray(frameSize);
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', this.remoteProcessInfo);
1322
+ this.emit('ready', connection.remoteProcessInfo);
1323
+ this.emit('connectionReady', { connection, remoteProcessInfo: connection.remoteProcessInfo });
669
1324
  break;
670
1325
  case 'BusJoined':
671
- this.emit('busJoined', message.value);
1326
+ this.#onRemoteBusJoined(connection, message.value);
672
1327
  break;
673
1328
  case 'BusLeft':
674
- this.emit('busLeft', message.value);
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
- this.socket?.destroy(error);
1340
+ connection.socket?.destroy(error);
686
1341
  return;
687
1342
  }
688
1343
 
689
- this.remoteProcessInfo = hello.info;
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
- #onBusFrame(payload) {
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.#sendBusControl(busState, busState.participantId, {
1547
+ this.#sendBusControlToConnection(busState, connection, {
819
1548
  type: 'RemoteParticipantReady',
820
1549
  value: { id: remoteParticipantId }
821
1550
  });
822
- this.#restartInterestForRemote(busState);
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
- #sendBusControl(busState, to, message) {
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.#sendBusMessage(busState, to, busPayload);
1738
+ this.#sendBusMessageToConnection(busState, connection, busPayload);
835
1739
  }
836
1740
 
837
- #sendBusMessage(busState, to, busPayload) {
838
- const socket = this.#writableSocket();
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
- #writableSocket() {
852
- if (!this.socket || this.socket.destroyed || !this.socket.writable) {
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 this.socket;
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) {