sen-ether-client 0.1.6 → 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 +75 -7
- package/README.md +54 -2
- package/index.js +26 -0
- package/lib/bus.js +261 -1
- package/lib/client.js +1002 -96
- package/lib/discovery.js +42 -1
- package/lib/sen.js +101 -13
- package/lib/values.js +14 -0
- package/package.json +2 -2
package/lib/discovery.js
CHANGED
|
@@ -50,6 +50,26 @@ function resolveInterfaceAddress(value) {
|
|
|
50
50
|
return ipv4.address;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function multicastInterfaceCandidates(interfaceAddress) {
|
|
54
|
+
if (interfaceAddress) {
|
|
55
|
+
return [interfaceAddress];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const addresses = [];
|
|
60
|
+
for (const candidates of Object.values(os.networkInterfaces())) {
|
|
61
|
+
for (const item of candidates ?? []) {
|
|
62
|
+
if ((item.family === 'IPv4' || item.family === 4) && item.address) {
|
|
63
|
+
addresses.push(item.address);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(addresses)];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
function normalizeTcpRemote(socket) {
|
|
54
74
|
return {
|
|
55
75
|
address: socket.remoteAddress,
|
|
@@ -149,8 +169,29 @@ export class EtherDiscoveryScanner extends EventEmitter {
|
|
|
149
169
|
const onListening = () => {
|
|
150
170
|
socket.off('error', onError);
|
|
151
171
|
try {
|
|
152
|
-
|
|
172
|
+
const interfaces = multicastInterfaceCandidates(this.interfaceAddress);
|
|
173
|
+
if (interfaces.length) {
|
|
174
|
+
let joined = 0;
|
|
175
|
+
/** @type {Error | undefined} */
|
|
176
|
+
let firstError;
|
|
177
|
+
for (const interfaceAddress of interfaces) {
|
|
178
|
+
try {
|
|
179
|
+
socket.addMembership(this.group, interfaceAddress);
|
|
180
|
+
joined += 1;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
firstError ??= /** @type {Error} */ (error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!joined) {
|
|
186
|
+
throw firstError ?? new Error(`could not join multicast group ${this.group}`);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
socket.addMembership(this.group);
|
|
190
|
+
}
|
|
153
191
|
socket.setMulticastLoopback(true);
|
|
192
|
+
if (this.interfaceAddress) {
|
|
193
|
+
socket.setMulticastInterface(this.interfaceAddress);
|
|
194
|
+
}
|
|
154
195
|
} catch (error) {
|
|
155
196
|
reject(error);
|
|
156
197
|
return;
|
package/lib/sen.js
CHANGED
|
@@ -384,42 +384,57 @@ export class Sen extends EventEmitter {
|
|
|
384
384
|
|
|
385
385
|
async #connectSingle(config) {
|
|
386
386
|
const target = config.target ?? await this.#discoverTarget(config);
|
|
387
|
-
|
|
387
|
+
const activeNode = Boolean(config.session && (config.tcpHub || config.multicastDiscovery !== false));
|
|
388
|
+
if (!target && !activeNode) {
|
|
388
389
|
throw new Error('no SEN ether process matches the requested filters');
|
|
389
390
|
}
|
|
390
391
|
|
|
391
|
-
const sessionName = target
|
|
392
|
+
const sessionName = target?.session?.name ?? target?.info?.sessionName ?? config.session;
|
|
392
393
|
if (!sessionName) {
|
|
393
394
|
throw new Error('cannot connect without a SEN session name');
|
|
394
395
|
}
|
|
395
|
-
if (!this.targets.includes(target)) {
|
|
396
|
+
if (target && !this.targets.includes(target)) {
|
|
396
397
|
this.targets.push(target);
|
|
397
398
|
}
|
|
398
|
-
if (!this.targetsBySession.has(sessionName)) {
|
|
399
|
+
if (target && !this.targetsBySession.has(sessionName)) {
|
|
399
400
|
this.targetsBySession.set(sessionName, target);
|
|
400
401
|
}
|
|
401
402
|
|
|
402
403
|
const client = new EtherClient({
|
|
403
404
|
sessionName,
|
|
404
405
|
appName: config.appName,
|
|
406
|
+
tcpHub: config.tcpHub,
|
|
407
|
+
multicastDiscovery: config.multicastDiscovery,
|
|
408
|
+
listen: config.listen,
|
|
409
|
+
listenHost: config.listenHost,
|
|
410
|
+
listenPort: config.listenPort,
|
|
411
|
+
advertisedHost: config.advertisedHost,
|
|
412
|
+
beamPeriodMs: config.beamPeriodMs,
|
|
405
413
|
socketKeepAlive: config.socketKeepAlive,
|
|
406
414
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
407
415
|
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
408
416
|
interfaceAddress: config.interfaceAddress,
|
|
417
|
+
group: config.group,
|
|
418
|
+
bindAddress: config.bindAddress,
|
|
409
419
|
discoveryPort: config.port,
|
|
410
420
|
busMulticast: config.busMulticast,
|
|
411
421
|
busMulticastPort: config.busMulticastPort,
|
|
412
422
|
busMulticastRange: config.busMulticastRange
|
|
413
423
|
});
|
|
414
424
|
this.client = client;
|
|
415
|
-
this.target = target;
|
|
425
|
+
this.target = target ?? { session: { name: sessionName }, process: client.processInfo, info: client.processInfo, local: true };
|
|
416
426
|
this.#wireClient(client);
|
|
417
427
|
|
|
418
428
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
429
|
+
if (config.tcpHub || config.listen !== false) {
|
|
430
|
+
await client.start(config);
|
|
431
|
+
}
|
|
432
|
+
if (target) {
|
|
433
|
+
await client.connect(target);
|
|
434
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
435
|
+
this.#startPresenceWatchdog(target, config);
|
|
436
|
+
}
|
|
437
|
+
this.emit('connect', { target: this.target, sessionName });
|
|
423
438
|
return this;
|
|
424
439
|
} catch (error) {
|
|
425
440
|
await client.close().catch(closeError => this.emit('warning', closeError));
|
|
@@ -534,6 +549,60 @@ export class Sen extends EventEmitter {
|
|
|
534
549
|
return await this.subscribe(name, options);
|
|
535
550
|
}
|
|
536
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Publish local JavaScript objects on a SEN bus.
|
|
554
|
+
*
|
|
555
|
+
* @param {string} busName Session-qualified or ether-local bus name.
|
|
556
|
+
* @param {object|object[]} objects
|
|
557
|
+
* @param {object} [options]
|
|
558
|
+
*/
|
|
559
|
+
async publishObjects(busName, objects, options = {}) {
|
|
560
|
+
if (!this.client) {
|
|
561
|
+
const sessionName = this.#sessionNameForBus(busName, options);
|
|
562
|
+
const session = await this.session(sessionName);
|
|
563
|
+
return await session.publishObjects(busName, objects, options);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!this.client || !this.target) {
|
|
567
|
+
throw new Error('Sen is not connected');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const sessionName = this.target.session?.name ?? this.client.processInfo.sessionName;
|
|
571
|
+
this.#assertBusBelongsToSession(busName, sessionName);
|
|
572
|
+
const bus = etherBusName(sessionName, busName);
|
|
573
|
+
|
|
574
|
+
if (!this.buses.has(bus)) {
|
|
575
|
+
const joined = await this.client.joinBus(bus, options);
|
|
576
|
+
this.buses.set(bus, new SenBus(this, bus, joined.busId));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return this.client.publishObjects(bus, objects, options);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Remove previously published local JavaScript objects from a SEN bus.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} busName Session-qualified or ether-local bus name.
|
|
586
|
+
* @param {Array<string|number>|string|number} objects Object ids or names.
|
|
587
|
+
* @param {object} [options]
|
|
588
|
+
*/
|
|
589
|
+
async removePublishedObjects(busName, objects, options = {}) {
|
|
590
|
+
if (!this.client) {
|
|
591
|
+
const sessionName = this.#sessionNameForBus(busName, options);
|
|
592
|
+
const session = await this.session(sessionName);
|
|
593
|
+
return await session.removePublishedObjects(busName, objects, options);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!this.client || !this.target) {
|
|
597
|
+
throw new Error('Sen is not connected');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const sessionName = this.target.session?.name ?? this.client.processInfo.sessionName;
|
|
601
|
+
this.#assertBusBelongsToSession(busName, sessionName);
|
|
602
|
+
const bus = etherBusName(sessionName, busName);
|
|
603
|
+
return this.client.removePublishedObjects(bus, objects);
|
|
604
|
+
}
|
|
605
|
+
|
|
537
606
|
async session(name) {
|
|
538
607
|
const sessionName = String(name || '').trim();
|
|
539
608
|
if (!sessionName) {
|
|
@@ -970,17 +1039,36 @@ export class Sen extends EventEmitter {
|
|
|
970
1039
|
client = new EtherClient({
|
|
971
1040
|
sessionName,
|
|
972
1041
|
appName: config.appName,
|
|
1042
|
+
tcpHub: config.tcpHub,
|
|
1043
|
+
multicastDiscovery: config.multicastDiscovery,
|
|
1044
|
+
listen: config.listen,
|
|
1045
|
+
listenHost: config.listenHost,
|
|
1046
|
+
listenPort: config.listenPort,
|
|
1047
|
+
advertisedHost: config.advertisedHost,
|
|
1048
|
+
beamPeriodMs: config.beamPeriodMs,
|
|
973
1049
|
socketKeepAlive: config.socketKeepAlive,
|
|
974
1050
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
975
|
-
socketIdleTimeoutMs: config.socketIdleTimeoutMs
|
|
1051
|
+
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
1052
|
+
interfaceAddress: config.interfaceAddress,
|
|
1053
|
+
group: config.group,
|
|
1054
|
+
bindAddress: config.bindAddress,
|
|
1055
|
+
discoveryPort: config.port,
|
|
1056
|
+
busMulticast: config.busMulticast,
|
|
1057
|
+
busMulticastPort: config.busMulticastPort,
|
|
1058
|
+
busMulticastRange: config.busMulticastRange
|
|
976
1059
|
});
|
|
977
1060
|
this.client = client;
|
|
978
1061
|
this.target = target;
|
|
979
1062
|
this.#wireClient(client);
|
|
980
1063
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1064
|
+
if (config.tcpHub || config.listen !== false) {
|
|
1065
|
+
await client.start(config);
|
|
1066
|
+
}
|
|
1067
|
+
if (target) {
|
|
1068
|
+
await client.connect(target);
|
|
1069
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
1070
|
+
this.#startPresenceWatchdog(target, config);
|
|
1071
|
+
}
|
|
984
1072
|
|
|
985
1073
|
for (const bus of this.buses.values()) {
|
|
986
1074
|
await bus.rejoin(config.timeout ?? 3000);
|
package/lib/values.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SenBinaryReader, SenBinaryWriter } from './codec.js';
|
|
2
2
|
import { decodePropertyUpdateBuffer } from './bus.js';
|
|
3
|
+
import { propertyHash } from './hash32.js';
|
|
3
4
|
|
|
4
5
|
function numberOrBigInt(value) {
|
|
5
6
|
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
|
@@ -323,6 +324,19 @@ export function encodeValue(value, typeName, typeRegistry) {
|
|
|
323
324
|
return writer.toBuffer();
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
export function encodePropertyUpdateBuffer(updates = [], typeRegistry) {
|
|
328
|
+
const writer = new SenBinaryWriter();
|
|
329
|
+
for (const update of updates) {
|
|
330
|
+
writer.writeUInt32(update.id ?? propertyHash(update.name));
|
|
331
|
+
const value = update.valueBuffer
|
|
332
|
+
? Buffer.from(update.valueBuffer)
|
|
333
|
+
: encodeValue(update.value, update.type, typeRegistry);
|
|
334
|
+
writer.writeUInt32(value.length);
|
|
335
|
+
writer.chunks.push(value);
|
|
336
|
+
}
|
|
337
|
+
return writer.toBuffer();
|
|
338
|
+
}
|
|
339
|
+
|
|
326
340
|
export function encodeArguments(values, argSpecs = [], typeRegistry) {
|
|
327
341
|
if ((values?.length ?? 0) !== argSpecs.length) {
|
|
328
342
|
throw new TypeError(`SEN method expects ${argSpecs.length} argument(s), got ${values?.length ?? 0}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sen-ether-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pure JavaScript SEN client for existing kernels over ether",
|
|
5
5
|
"senCompatibility": {
|
|
6
6
|
"kernelProtocolVersion": 9,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"exports": "./index.js",
|
|
26
26
|
"scripts": {
|
|
27
27
|
"generate:protocol": "node ./scripts/generate-protocol.mjs",
|
|
28
|
-
"test": "node --test ./test/protocol.test.js ./test/codec.test.js ./test/discovery.test.js ./test/sen.test.js",
|
|
28
|
+
"test": "node --test --test-concurrency=1 --test-reporter spec ./test/protocol.test.js ./test/codec.test.js ./test/discovery.test.js ./test/client.test.js ./test/sen.test.js",
|
|
29
29
|
"test:integration": "node --test ./test/integration-*.test.js"
|
|
30
30
|
}
|
|
31
31
|
}
|