sen-ether-client 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +69 -6
- package/README.md +54 -2
- package/index.js +26 -0
- package/lib/bus.js +261 -1
- package/lib/client.js +1016 -97
- package/lib/sen.js +164 -46
- package/lib/values.js +14 -0
- package/package.json +2 -2
package/lib/sen.js
CHANGED
|
@@ -161,8 +161,20 @@ function normalizeTimestampNs(value) {
|
|
|
161
161
|
return typeof value === 'bigint' ? value : BigInt(value);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function stateRequestKey(interestId, objectId) {
|
|
165
|
-
return `${interestId >>> 0}:${objectId
|
|
164
|
+
function stateRequestKey(interestId, ownerId, objectId) {
|
|
165
|
+
return `${interestId >>> 0}:${remoteObjectKey(ownerId, objectId)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function remoteObjectKey(ownerId, objectId) {
|
|
169
|
+
const owner = ownerId === undefined || ownerId === null ? 'unknown' : String(ownerId >>> 0);
|
|
170
|
+
return `${owner}:${objectId >>> 0}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function eventOwnerId(event) {
|
|
174
|
+
if (event?.ownerId !== undefined) {
|
|
175
|
+
return event.ownerId;
|
|
176
|
+
}
|
|
177
|
+
return event?.multicast ? undefined : event?.to;
|
|
166
178
|
}
|
|
167
179
|
|
|
168
180
|
async function waitForSessionBuses(session, timeoutMs) {
|
|
@@ -384,42 +396,57 @@ export class Sen extends EventEmitter {
|
|
|
384
396
|
|
|
385
397
|
async #connectSingle(config) {
|
|
386
398
|
const target = config.target ?? await this.#discoverTarget(config);
|
|
387
|
-
|
|
399
|
+
const activeNode = Boolean(config.session && (config.tcpHub || config.multicastDiscovery !== false));
|
|
400
|
+
if (!target && !activeNode) {
|
|
388
401
|
throw new Error('no SEN ether process matches the requested filters');
|
|
389
402
|
}
|
|
390
403
|
|
|
391
|
-
const sessionName = target
|
|
404
|
+
const sessionName = target?.session?.name ?? target?.info?.sessionName ?? config.session;
|
|
392
405
|
if (!sessionName) {
|
|
393
406
|
throw new Error('cannot connect without a SEN session name');
|
|
394
407
|
}
|
|
395
|
-
if (!this.targets.includes(target)) {
|
|
408
|
+
if (target && !this.targets.includes(target)) {
|
|
396
409
|
this.targets.push(target);
|
|
397
410
|
}
|
|
398
|
-
if (!this.targetsBySession.has(sessionName)) {
|
|
411
|
+
if (target && !this.targetsBySession.has(sessionName)) {
|
|
399
412
|
this.targetsBySession.set(sessionName, target);
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
const client = new EtherClient({
|
|
403
416
|
sessionName,
|
|
404
417
|
appName: config.appName,
|
|
418
|
+
tcpHub: config.tcpHub,
|
|
419
|
+
multicastDiscovery: config.multicastDiscovery,
|
|
420
|
+
listen: config.listen,
|
|
421
|
+
listenHost: config.listenHost,
|
|
422
|
+
listenPort: config.listenPort,
|
|
423
|
+
advertisedHost: config.advertisedHost,
|
|
424
|
+
beamPeriodMs: config.beamPeriodMs,
|
|
405
425
|
socketKeepAlive: config.socketKeepAlive,
|
|
406
426
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
407
427
|
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
408
428
|
interfaceAddress: config.interfaceAddress,
|
|
429
|
+
group: config.group,
|
|
430
|
+
bindAddress: config.bindAddress,
|
|
409
431
|
discoveryPort: config.port,
|
|
410
432
|
busMulticast: config.busMulticast,
|
|
411
433
|
busMulticastPort: config.busMulticastPort,
|
|
412
434
|
busMulticastRange: config.busMulticastRange
|
|
413
435
|
});
|
|
414
436
|
this.client = client;
|
|
415
|
-
this.target = target;
|
|
437
|
+
this.target = target ?? { session: { name: sessionName }, process: client.processInfo, info: client.processInfo, local: true };
|
|
416
438
|
this.#wireClient(client);
|
|
417
439
|
|
|
418
440
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
441
|
+
if (config.tcpHub || config.listen !== false) {
|
|
442
|
+
await client.start(config);
|
|
443
|
+
}
|
|
444
|
+
if (target) {
|
|
445
|
+
await client.connect(target);
|
|
446
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
447
|
+
this.#startPresenceWatchdog(target, config);
|
|
448
|
+
}
|
|
449
|
+
this.emit('connect', { target: this.target, sessionName });
|
|
423
450
|
return this;
|
|
424
451
|
} catch (error) {
|
|
425
452
|
await client.close().catch(closeError => this.emit('warning', closeError));
|
|
@@ -534,6 +561,60 @@ export class Sen extends EventEmitter {
|
|
|
534
561
|
return await this.subscribe(name, options);
|
|
535
562
|
}
|
|
536
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Publish local JavaScript objects on a SEN bus.
|
|
566
|
+
*
|
|
567
|
+
* @param {string} busName Session-qualified or ether-local bus name.
|
|
568
|
+
* @param {object|object[]} objects
|
|
569
|
+
* @param {object} [options]
|
|
570
|
+
*/
|
|
571
|
+
async publishObjects(busName, objects, options = {}) {
|
|
572
|
+
if (!this.client) {
|
|
573
|
+
const sessionName = this.#sessionNameForBus(busName, options);
|
|
574
|
+
const session = await this.session(sessionName);
|
|
575
|
+
return await session.publishObjects(busName, objects, options);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!this.client || !this.target) {
|
|
579
|
+
throw new Error('Sen is not connected');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const sessionName = this.target.session?.name ?? this.client.processInfo.sessionName;
|
|
583
|
+
this.#assertBusBelongsToSession(busName, sessionName);
|
|
584
|
+
const bus = etherBusName(sessionName, busName);
|
|
585
|
+
|
|
586
|
+
if (!this.buses.has(bus)) {
|
|
587
|
+
const joined = await this.client.joinBus(bus, options);
|
|
588
|
+
this.buses.set(bus, new SenBus(this, bus, joined.busId));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return this.client.publishObjects(bus, objects, options);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Remove previously published local JavaScript objects from a SEN bus.
|
|
596
|
+
*
|
|
597
|
+
* @param {string} busName Session-qualified or ether-local bus name.
|
|
598
|
+
* @param {Array<string|number>|string|number} objects Object ids or names.
|
|
599
|
+
* @param {object} [options]
|
|
600
|
+
*/
|
|
601
|
+
async removePublishedObjects(busName, objects, options = {}) {
|
|
602
|
+
if (!this.client) {
|
|
603
|
+
const sessionName = this.#sessionNameForBus(busName, options);
|
|
604
|
+
const session = await this.session(sessionName);
|
|
605
|
+
return await session.removePublishedObjects(busName, objects, options);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!this.client || !this.target) {
|
|
609
|
+
throw new Error('Sen is not connected');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const sessionName = this.target.session?.name ?? this.client.processInfo.sessionName;
|
|
613
|
+
this.#assertBusBelongsToSession(busName, sessionName);
|
|
614
|
+
const bus = etherBusName(sessionName, busName);
|
|
615
|
+
return this.client.removePublishedObjects(bus, objects);
|
|
616
|
+
}
|
|
617
|
+
|
|
537
618
|
async session(name) {
|
|
538
619
|
const sessionName = String(name || '').trim();
|
|
539
620
|
if (!sessionName) {
|
|
@@ -970,17 +1051,36 @@ export class Sen extends EventEmitter {
|
|
|
970
1051
|
client = new EtherClient({
|
|
971
1052
|
sessionName,
|
|
972
1053
|
appName: config.appName,
|
|
1054
|
+
tcpHub: config.tcpHub,
|
|
1055
|
+
multicastDiscovery: config.multicastDiscovery,
|
|
1056
|
+
listen: config.listen,
|
|
1057
|
+
listenHost: config.listenHost,
|
|
1058
|
+
listenPort: config.listenPort,
|
|
1059
|
+
advertisedHost: config.advertisedHost,
|
|
1060
|
+
beamPeriodMs: config.beamPeriodMs,
|
|
973
1061
|
socketKeepAlive: config.socketKeepAlive,
|
|
974
1062
|
socketKeepAliveInitialDelayMs: config.socketKeepAliveInitialDelayMs,
|
|
975
|
-
socketIdleTimeoutMs: config.socketIdleTimeoutMs
|
|
1063
|
+
socketIdleTimeoutMs: config.socketIdleTimeoutMs,
|
|
1064
|
+
interfaceAddress: config.interfaceAddress,
|
|
1065
|
+
group: config.group,
|
|
1066
|
+
bindAddress: config.bindAddress,
|
|
1067
|
+
discoveryPort: config.port,
|
|
1068
|
+
busMulticast: config.busMulticast,
|
|
1069
|
+
busMulticastPort: config.busMulticastPort,
|
|
1070
|
+
busMulticastRange: config.busMulticastRange
|
|
976
1071
|
});
|
|
977
1072
|
this.client = client;
|
|
978
1073
|
this.target = target;
|
|
979
1074
|
this.#wireClient(client);
|
|
980
1075
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1076
|
+
if (config.tcpHub || config.listen !== false) {
|
|
1077
|
+
await client.start(config);
|
|
1078
|
+
}
|
|
1079
|
+
if (target) {
|
|
1080
|
+
await client.connect(target);
|
|
1081
|
+
await waitForEvent(client, 'ready', config.timeout ?? 3000);
|
|
1082
|
+
this.#startPresenceWatchdog(target, config);
|
|
1083
|
+
}
|
|
984
1084
|
|
|
985
1085
|
for (const bus of this.buses.values()) {
|
|
986
1086
|
await bus.rejoin(config.timeout ?? 3000);
|
|
@@ -1224,43 +1324,36 @@ export class SenBus extends EventEmitter {
|
|
|
1224
1324
|
}
|
|
1225
1325
|
|
|
1226
1326
|
handleObjectsPublished(event) {
|
|
1327
|
+
const ownerId = eventOwnerId(event);
|
|
1227
1328
|
const newTypeHashes = new Set();
|
|
1228
1329
|
for (const discovery of event.discoveries ?? []) {
|
|
1229
1330
|
const interest = this.interests.get(discovery.interestId);
|
|
1230
|
-
if (interest
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1331
|
+
if (!interest) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (ownerId !== undefined) {
|
|
1335
|
+
interest.ownerId ??= ownerId;
|
|
1336
|
+
interest.ownerIds.add(ownerId >>> 0);
|
|
1236
1337
|
}
|
|
1237
1338
|
|
|
1238
1339
|
for (const info of discovery.objects ?? []) {
|
|
1239
|
-
let object = this
|
|
1240
|
-
if (object && event.ownerId !== undefined && object.ownerId !== event.ownerId) {
|
|
1241
|
-
this.#removeObjectFromAllInterests(object, {
|
|
1242
|
-
reason: 'ownerChanged',
|
|
1243
|
-
ownerId: event.ownerId,
|
|
1244
|
-
previousOwnerId: object.ownerId
|
|
1245
|
-
});
|
|
1246
|
-
object = undefined;
|
|
1247
|
-
}
|
|
1340
|
+
let object = this.#objectByOwnerAndId(ownerId, info.id);
|
|
1248
1341
|
const isNewObject = !object;
|
|
1249
1342
|
if (!object) {
|
|
1250
1343
|
object = new SenRemoteObject(this, {
|
|
1251
1344
|
...info,
|
|
1252
|
-
ownerId
|
|
1345
|
+
ownerId,
|
|
1253
1346
|
interestId: discovery.interestId
|
|
1254
1347
|
});
|
|
1255
|
-
this.objectsById.set(object.
|
|
1348
|
+
this.objectsById.set(object.key, object);
|
|
1256
1349
|
} else {
|
|
1257
1350
|
object.attachInterest(discovery.interestId);
|
|
1258
1351
|
object.updateDiscoveryInfo({
|
|
1259
1352
|
...info,
|
|
1260
|
-
ownerId
|
|
1353
|
+
ownerId
|
|
1261
1354
|
});
|
|
1262
1355
|
}
|
|
1263
|
-
interest?.objectsById.set(object.
|
|
1356
|
+
interest?.objectsById.set(object.key, object);
|
|
1264
1357
|
if (info.state?.length) {
|
|
1265
1358
|
object.applyState(info.state, 'published', info.time, { interestId: discovery.interestId });
|
|
1266
1359
|
}
|
|
@@ -1279,10 +1372,14 @@ export class SenBus extends EventEmitter {
|
|
|
1279
1372
|
}
|
|
1280
1373
|
|
|
1281
1374
|
handleObjectsRemoved(event) {
|
|
1375
|
+
const ownerId = eventOwnerId(event);
|
|
1282
1376
|
for (const removal of event.removals ?? []) {
|
|
1377
|
+
const interest = this.interests.get(removal.interestId);
|
|
1378
|
+
if (!interest) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1283
1381
|
for (const id of removal.ids ?? []) {
|
|
1284
|
-
const object = this
|
|
1285
|
-
const interest = this.interests.get(removal.interestId);
|
|
1382
|
+
const object = this.#objectByOwnerAndId(ownerId, id);
|
|
1286
1383
|
if (object) {
|
|
1287
1384
|
this.#removeObjectFromInterest(object, removal.interestId, interest);
|
|
1288
1385
|
}
|
|
@@ -1331,14 +1428,16 @@ export class SenBus extends EventEmitter {
|
|
|
1331
1428
|
|
|
1332
1429
|
#removeObjectFromInterest(object, interestId, interest, detail = {}) {
|
|
1333
1430
|
const normalizedInterestId = interestId >>> 0;
|
|
1334
|
-
this.stateRequestedObjectIds.delete(stateRequestKey(normalizedInterestId, object.id));
|
|
1335
|
-
interest?.objectsById.delete(object.
|
|
1431
|
+
this.stateRequestedObjectIds.delete(stateRequestKey(normalizedInterestId, object.ownerId, object.id));
|
|
1432
|
+
interest?.objectsById.delete(object.key);
|
|
1336
1433
|
object.detachInterest(normalizedInterestId);
|
|
1337
1434
|
object.emit('remove', { interestId: normalizedInterestId, ...detail });
|
|
1338
1435
|
interest?.emit('remove', object);
|
|
1339
1436
|
if (object.interestIds.size === 0) {
|
|
1340
|
-
this.objectsById.delete(object.
|
|
1341
|
-
this.
|
|
1437
|
+
this.objectsById.delete(object.key);
|
|
1438
|
+
if (![...this.objectsById.values()].some(item => item.typeHash === object.typeHash)) {
|
|
1439
|
+
this.requestedTypeHashes.delete(object.typeHash);
|
|
1440
|
+
}
|
|
1342
1441
|
this.emit('remove', object);
|
|
1343
1442
|
this.sen.emit('remove', object);
|
|
1344
1443
|
}
|
|
@@ -1403,10 +1502,15 @@ export class SenBus extends EventEmitter {
|
|
|
1403
1502
|
}
|
|
1404
1503
|
|
|
1405
1504
|
handleObjectsStateResponse(event) {
|
|
1505
|
+
const ownerId = eventOwnerId(event);
|
|
1406
1506
|
for (const response of event.responses ?? []) {
|
|
1507
|
+
const interest = this.interests.get(response.interestId);
|
|
1508
|
+
if (!interest) {
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1407
1511
|
for (const state of response.objectStates ?? []) {
|
|
1408
|
-
const object = this
|
|
1409
|
-
if (!object || (
|
|
1512
|
+
const object = this.#objectByOwnerAndId(ownerId, state.id);
|
|
1513
|
+
if (!object || !interest.objectsById.has(object.key)) {
|
|
1410
1514
|
continue;
|
|
1411
1515
|
}
|
|
1412
1516
|
object.applyState(state.state, 'state', state.timestamp, { interestId: response.interestId });
|
|
@@ -1415,7 +1519,7 @@ export class SenBus extends EventEmitter {
|
|
|
1415
1519
|
}
|
|
1416
1520
|
|
|
1417
1521
|
handleRuntimeObjectUpdate(event) {
|
|
1418
|
-
const object = this
|
|
1522
|
+
const object = this.#objectByOwnerAndId(eventOwnerId(event), event.update.objectId);
|
|
1419
1523
|
if (!object) {
|
|
1420
1524
|
return;
|
|
1421
1525
|
}
|
|
@@ -1423,8 +1527,9 @@ export class SenBus extends EventEmitter {
|
|
|
1423
1527
|
}
|
|
1424
1528
|
|
|
1425
1529
|
handleRuntimeEvents(event) {
|
|
1530
|
+
const ownerId = eventOwnerId(event);
|
|
1426
1531
|
for (const item of event.events ?? []) {
|
|
1427
|
-
const object = this
|
|
1532
|
+
const object = this.#objectByOwnerAndId(ownerId, item.producerId);
|
|
1428
1533
|
if (!object) {
|
|
1429
1534
|
continue;
|
|
1430
1535
|
}
|
|
@@ -1495,7 +1600,7 @@ export class SenBus extends EventEmitter {
|
|
|
1495
1600
|
if (!object.spec) {
|
|
1496
1601
|
continue;
|
|
1497
1602
|
}
|
|
1498
|
-
const key = stateRequestKey(interest.id, object.id);
|
|
1603
|
+
const key = stateRequestKey(interest.id, object.ownerId, object.id);
|
|
1499
1604
|
if (!force && this.stateRequestedObjectIds.has(key)) {
|
|
1500
1605
|
continue;
|
|
1501
1606
|
}
|
|
@@ -1568,6 +1673,14 @@ export class SenBus extends EventEmitter {
|
|
|
1568
1673
|
}
|
|
1569
1674
|
}
|
|
1570
1675
|
}
|
|
1676
|
+
|
|
1677
|
+
#objectByOwnerAndId(ownerId, objectId) {
|
|
1678
|
+
if (ownerId !== undefined && ownerId !== null) {
|
|
1679
|
+
return this.objectsById.get(remoteObjectKey(ownerId, objectId));
|
|
1680
|
+
}
|
|
1681
|
+
const id = objectId >>> 0;
|
|
1682
|
+
return [...this.objectsById.values()].find(object => object.id === id);
|
|
1683
|
+
}
|
|
1571
1684
|
}
|
|
1572
1685
|
|
|
1573
1686
|
export class SenInterest extends EventEmitter {
|
|
@@ -1577,6 +1690,7 @@ export class SenInterest extends EventEmitter {
|
|
|
1577
1690
|
this.id = id;
|
|
1578
1691
|
this.query = query;
|
|
1579
1692
|
this.ownerId = undefined;
|
|
1693
|
+
this.ownerIds = new Set();
|
|
1580
1694
|
this.options = { ...options };
|
|
1581
1695
|
this.propertyNames = normalizePropertyNames(options.properties ?? options.propertyNames);
|
|
1582
1696
|
this.changeMode = options.changeMode ?? (options.batch ? 'batch' : 'individual');
|
|
@@ -1779,6 +1893,10 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1779
1893
|
return this.propertyTimestamps.get(name);
|
|
1780
1894
|
}
|
|
1781
1895
|
|
|
1896
|
+
get key() {
|
|
1897
|
+
return remoteObjectKey(this.ownerId, this.id);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1782
1900
|
isReadyForInterest(interestId) {
|
|
1783
1901
|
return this.readyInterestIds.has(interestId >>> 0);
|
|
1784
1902
|
}
|
|
@@ -1914,7 +2032,7 @@ export class SenRemoteObject extends EventEmitter {
|
|
|
1914
2032
|
|
|
1915
2033
|
const interests = [];
|
|
1916
2034
|
for (const interest of this.bus.interests.values()) {
|
|
1917
|
-
if (interest.objectsById.has(this.id)) {
|
|
2035
|
+
if (interest.objectsById.has(this.key) || interest.objectsById.has(this.id)) {
|
|
1918
2036
|
interests.push(interest);
|
|
1919
2037
|
}
|
|
1920
2038
|
}
|
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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
}
|