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/API.md +69 -6
- package/README.md +54 -2
- package/index.js +26 -0
- package/lib/bus.js +261 -1
- package/lib/client.js +1002 -96
- package/lib/sen.js +101 -13
- package/lib/values.js +14 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
encodeConfirmedBusFrame,
|
|
12
12
|
encodeRuntimeMethodCall
|
|
13
13
|
} from './bus.js';
|
|
14
|
+
import { encodePropertyUpdateBuffer } from './values.js';
|
|
14
15
|
import {
|
|
15
16
|
decodeEtherControlMessage,
|
|
16
17
|
decodeProcessTcpHeader,
|
|
18
|
+
decodeSessionPresenceBeam,
|
|
17
19
|
encodeEtherControlMessage,
|
|
18
20
|
encodeProcessTcpFrame,
|
|
21
|
+
encodeSessionPresenceBeam,
|
|
19
22
|
ETHER_PROTOCOL_VERSION,
|
|
20
23
|
KERNEL_PROTOCOL_VERSION,
|
|
21
24
|
PROCESS_MESSAGE_CATEGORY
|
|
@@ -24,8 +27,11 @@ import { crc32 } from './crc32.js';
|
|
|
24
27
|
|
|
25
28
|
const LINUX_OS_KIND = 1;
|
|
26
29
|
const X64_CPU_ARCH = 1;
|
|
30
|
+
const DEFAULT_DISCOVERY_GROUP = '239.255.0.44';
|
|
27
31
|
const DEFAULT_DISCOVERY_PORT = 60543;
|
|
28
32
|
const DEFAULT_BUS_MULTICAST_PORT = 50985;
|
|
33
|
+
const TCP_DISCOVERY_BEAM_SIZE = 508;
|
|
34
|
+
const DEFAULT_BEAM_PERIOD_MS = 1000;
|
|
29
35
|
const BUS_HASH_SEED = 15071983;
|
|
30
36
|
const FNV1A_OFFSET_BASIS = 0x811c9dc5;
|
|
31
37
|
const FNV1A_PRIME = 0x01000193;
|
|
@@ -169,6 +175,183 @@ function computeBusMulticastGroup(sessionId, busId, discoveryPort, ranges) {
|
|
|
169
175
|
return bytes.map((byte, index) => Math.min(Math.max(byte, ranges[index].min), ranges[index].max)).join('.');
|
|
170
176
|
}
|
|
171
177
|
|
|
178
|
+
function classSpecData(spec) {
|
|
179
|
+
return spec?.data?.type === 'ClassTypeSpec' ? spec.data.value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function localTypeSpec(typeRegistry, typeName) {
|
|
183
|
+
return typeRegistry?.get?.(typeName) ?? typeRegistry?.[typeName];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function collectClassProperties(spec, typeRegistry, seen = new Set()) {
|
|
187
|
+
const data = classSpecData(spec);
|
|
188
|
+
const key = spec?.qualifiedName ?? spec?.name;
|
|
189
|
+
if (!data || seen.has(key)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
seen.add(key);
|
|
193
|
+
return [
|
|
194
|
+
...(data.parents ?? []).flatMap(parent => collectClassProperties(localTypeSpec(typeRegistry, parent), typeRegistry, seen)),
|
|
195
|
+
...(data.properties ?? [])
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function inferValueType(value) {
|
|
200
|
+
if (typeof value === 'boolean') return 'bool';
|
|
201
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'i64' : 'f64';
|
|
202
|
+
if (typeof value === 'bigint') return 'i64';
|
|
203
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer) return 'Buffer';
|
|
204
|
+
if (typeof value === 'string' || value === null || value === undefined) return 'string';
|
|
205
|
+
throw new TypeError('cannot infer SEN type for structured value; pass an explicit spec and dependent types');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ensureClassSpec(className, state = {}, spec) {
|
|
209
|
+
if (spec) return spec;
|
|
210
|
+
const properties = Object.entries(state || {}).map(([name, value]) => ({
|
|
211
|
+
name,
|
|
212
|
+
description: '',
|
|
213
|
+
category: 'dynamicRO',
|
|
214
|
+
type: inferValueType(value),
|
|
215
|
+
transportMode: 'confirmed',
|
|
216
|
+
tags: [],
|
|
217
|
+
checkedSet: false
|
|
218
|
+
}));
|
|
219
|
+
return {
|
|
220
|
+
name: String(className || '').split('.').pop() || String(className || ''),
|
|
221
|
+
qualifiedName: className,
|
|
222
|
+
description: '',
|
|
223
|
+
data: {
|
|
224
|
+
type: 'ClassTypeSpec',
|
|
225
|
+
value: {
|
|
226
|
+
properties,
|
|
227
|
+
methods: [],
|
|
228
|
+
events: [],
|
|
229
|
+
constructor: { name: '', description: '', args: [], returnType: '' },
|
|
230
|
+
parents: [],
|
|
231
|
+
isInterface: false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeTypeDefinitions(typeDefinitions = []) {
|
|
238
|
+
const values = typeDefinitions instanceof Map
|
|
239
|
+
? [...typeDefinitions.values()]
|
|
240
|
+
: Array.isArray(typeDefinitions)
|
|
241
|
+
? typeDefinitions
|
|
242
|
+
: Object.values(typeDefinitions || {});
|
|
243
|
+
return values.filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function processKeyFromInfo(info = {}) {
|
|
247
|
+
return `${info.hostId}:${info.processId}:${info.sessionId}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isSameProcessInfo(a = {}, b = {}) {
|
|
251
|
+
return (
|
|
252
|
+
(a.hostId >>> 0) === (b.hostId >>> 0) &&
|
|
253
|
+
(a.processId >>> 0) === (b.processId >>> 0) &&
|
|
254
|
+
(a.sessionId >>> 0) === (b.sessionId >>> 0)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function firstAdvertisableAddress(interfaceAddress) {
|
|
259
|
+
if (interfaceAddress && interfaceAddress !== '0.0.0.0') {
|
|
260
|
+
return interfaceAddress;
|
|
261
|
+
}
|
|
262
|
+
for (const candidates of Object.values(os.networkInterfaces())) {
|
|
263
|
+
for (const item of candidates ?? []) {
|
|
264
|
+
if ((item.family === 'IPv4' || item.family === 4) && !item.internal && item.address) {
|
|
265
|
+
return item.address;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return '127.0.0.1';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseHostPort(value, fallbackPort) {
|
|
273
|
+
if (!value) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (typeof value === 'object') {
|
|
277
|
+
return { host: value.host ?? '127.0.0.1', port: Number(value.port ?? fallbackPort) };
|
|
278
|
+
}
|
|
279
|
+
const text = String(value).trim();
|
|
280
|
+
const idx = text.lastIndexOf(':');
|
|
281
|
+
if (idx <= 0) {
|
|
282
|
+
return { host: text || '127.0.0.1', port: Number(fallbackPort) };
|
|
283
|
+
}
|
|
284
|
+
return { host: text.slice(0, idx), port: Number(text.slice(idx + 1)) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function padDiscoveryBeam(buffer) {
|
|
288
|
+
if (buffer.length > TCP_DISCOVERY_BEAM_SIZE) {
|
|
289
|
+
throw new Error(`SEN discovery beam is too large: ${buffer.length} > ${TCP_DISCOVERY_BEAM_SIZE}`);
|
|
290
|
+
}
|
|
291
|
+
if (buffer.length === TCP_DISCOVERY_BEAM_SIZE) {
|
|
292
|
+
return buffer;
|
|
293
|
+
}
|
|
294
|
+
const padded = Buffer.alloc(TCP_DISCOVERY_BEAM_SIZE);
|
|
295
|
+
Buffer.from(buffer).copy(padded);
|
|
296
|
+
return padded;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function withCloseTimeout(register, timeoutMs = 1000) {
|
|
300
|
+
return new Promise(resolve => {
|
|
301
|
+
let done = false;
|
|
302
|
+
const finish = () => {
|
|
303
|
+
if (done) return;
|
|
304
|
+
done = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
resolve();
|
|
307
|
+
};
|
|
308
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
309
|
+
timer.unref?.();
|
|
310
|
+
try {
|
|
311
|
+
register(finish);
|
|
312
|
+
} catch {
|
|
313
|
+
finish();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildLocalObject(input, typeRegistry) {
|
|
319
|
+
const className = String(input.className ?? input.classname ?? input.type ?? '').trim();
|
|
320
|
+
if (!className) {
|
|
321
|
+
throw new TypeError('SEN published object requires className');
|
|
322
|
+
}
|
|
323
|
+
const name = String(input.name ?? '').trim();
|
|
324
|
+
if (!name) {
|
|
325
|
+
throw new TypeError('SEN published object requires name');
|
|
326
|
+
}
|
|
327
|
+
const id = input.id ?? crc32(name);
|
|
328
|
+
const typeHash = input.typeHash ?? crc32(className);
|
|
329
|
+
const state = input.state ?? input.snapshot ?? input.properties ?? {};
|
|
330
|
+
const spec = ensureClassSpec(className, state, input.spec);
|
|
331
|
+
const registry = new Map(typeRegistry);
|
|
332
|
+
registry.set(spec.qualifiedName, spec);
|
|
333
|
+
const properties = collectClassProperties(spec, registry);
|
|
334
|
+
const stateBuffer = input.stateBuffer
|
|
335
|
+
? Buffer.from(input.stateBuffer)
|
|
336
|
+
: encodePropertyUpdateBuffer(
|
|
337
|
+
properties
|
|
338
|
+
.filter(property => Object.prototype.hasOwnProperty.call(state, property.name))
|
|
339
|
+
.map(property => ({ name: property.name, type: property.type, value: state[property.name] })),
|
|
340
|
+
registry
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
id: id >>> 0,
|
|
345
|
+
name,
|
|
346
|
+
className,
|
|
347
|
+
typeHash: typeHash >>> 0,
|
|
348
|
+
spec,
|
|
349
|
+
state,
|
|
350
|
+
stateBuffer,
|
|
351
|
+
timestamp: input.timestamp ?? input.time ?? BigInt(Date.now()) * 1_000_000n
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
172
355
|
/**
|
|
173
356
|
* Create a ProcessInfo compatible with sen::kernel::getOwnProcessInfo.
|
|
174
357
|
*
|
|
@@ -251,7 +434,15 @@ export class EtherClient extends EventEmitter {
|
|
|
251
434
|
socketKeepAlive: true,
|
|
252
435
|
socketKeepAliveInitialDelayMs: 1000,
|
|
253
436
|
socketIdleTimeoutMs: 0,
|
|
437
|
+
group: DEFAULT_DISCOVERY_GROUP,
|
|
438
|
+
bindAddress: undefined,
|
|
254
439
|
discoveryPort: discoveryPortFromEnv() ?? DEFAULT_DISCOVERY_PORT,
|
|
440
|
+
tcpHub: undefined,
|
|
441
|
+
listen: true,
|
|
442
|
+
listenHost: '0.0.0.0',
|
|
443
|
+
listenPort: 0,
|
|
444
|
+
advertisedHost: undefined,
|
|
445
|
+
beamPeriodMs: DEFAULT_BEAM_PERIOD_MS,
|
|
255
446
|
busMulticastPort: DEFAULT_BUS_MULTICAST_PORT,
|
|
256
447
|
busMulticastRange: DEFAULT_MULTICAST_RANGE,
|
|
257
448
|
...options
|
|
@@ -264,107 +455,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
|
-
*
|
|
476
|
+
* Start this JS process as an active Ether node.
|
|
275
477
|
*
|
|
276
|
-
*
|
|
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
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
509
|
+
server.off('listening', onListening);
|
|
290
510
|
reject(error);
|
|
291
511
|
};
|
|
292
512
|
const onListening = () => {
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
754
|
+
socket.off('connect', onConnect);
|
|
311
755
|
reject(error);
|
|
312
756
|
};
|
|
313
757
|
const onConnect = () => {
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
325
|
-
|
|
768
|
+
socket.once('error', onError);
|
|
769
|
+
socket.once('connect', onConnect);
|
|
326
770
|
});
|
|
327
771
|
|
|
328
772
|
return this;
|
|
329
773
|
}
|
|
330
774
|
|
|
331
|
-
#
|
|
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
|
-
|
|
833
|
+
socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
|
|
337
834
|
}
|
|
338
835
|
if (this.options.socketIdleTimeoutMs > 0) {
|
|
339
|
-
|
|
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
|
-
|
|
839
|
+
socket.destroy(error);
|
|
343
840
|
});
|
|
344
841
|
}
|
|
345
842
|
}
|
|
346
843
|
|
|
347
844
|
async close() {
|
|
348
|
-
const
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
1247
|
+
this.#sendControlPayloadToConnection(connection, payload);
|
|
614
1248
|
}
|
|
615
1249
|
|
|
616
|
-
#sendReady() {
|
|
617
|
-
this.#
|
|
1250
|
+
#sendReady(connection) {
|
|
1251
|
+
this.#sendControlPayloadToConnection(connection, encodeEtherControlMessage({ type: 'Ready' }));
|
|
618
1252
|
}
|
|
619
1253
|
|
|
620
|
-
#
|
|
621
|
-
const socket = this.#
|
|
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
|
-
#
|
|
630
|
-
|
|
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 (
|
|
633
|
-
const header = decodeProcessTcpHeader(
|
|
1272
|
+
while (connection.receiveBuffer.length >= 5) {
|
|
1273
|
+
const header = decodeProcessTcpHeader(connection.receiveBuffer);
|
|
634
1274
|
const frameSize = 5 + header.payloadSize;
|
|
635
|
-
if (
|
|
1275
|
+
if (connection.receiveBuffer.length < frameSize) {
|
|
636
1276
|
return;
|
|
637
1277
|
}
|
|
638
1278
|
|
|
639
|
-
const payload =
|
|
640
|
-
|
|
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',
|
|
1309
|
+
this.emit('ready', connection.remoteProcessInfo);
|
|
1310
|
+
this.emit('connectionReady', { connection, remoteProcessInfo: connection.remoteProcessInfo });
|
|
669
1311
|
break;
|
|
670
1312
|
case 'BusJoined':
|
|
671
|
-
this
|
|
1313
|
+
this.#onRemoteBusJoined(connection, message.value);
|
|
672
1314
|
break;
|
|
673
1315
|
case 'BusLeft':
|
|
674
|
-
this
|
|
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
|
-
|
|
1327
|
+
connection.socket?.destroy(error);
|
|
686
1328
|
return;
|
|
687
1329
|
}
|
|
688
1330
|
|
|
689
|
-
|
|
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
|
-
#
|
|
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.#
|
|
1534
|
+
this.#sendBusControlToConnection(busState, connection, {
|
|
819
1535
|
type: 'RemoteParticipantReady',
|
|
820
1536
|
value: { id: remoteParticipantId }
|
|
821
1537
|
});
|
|
822
|
-
this.#
|
|
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
|
-
#
|
|
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.#
|
|
1725
|
+
this.#sendBusMessageToConnection(busState, connection, busPayload);
|
|
835
1726
|
}
|
|
836
1727
|
|
|
837
|
-
#
|
|
838
|
-
|
|
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
|
-
#
|
|
852
|
-
|
|
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
|
|
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) {
|