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/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 >>> 0}`;
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
- if (!target) {
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.session?.name ?? target.info?.sessionName ?? config.session;
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
- await client.connect(target);
420
- await waitForEvent(client, 'ready', config.timeout ?? 3000);
421
- this.#startPresenceWatchdog(target, config);
422
- this.emit('connect', { target, sessionName });
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
- await client.connect(target);
982
- await waitForEvent(client, 'ready', config.timeout ?? 3000);
983
- this.#startPresenceWatchdog(target, config);
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 && event.ownerId !== undefined) {
1231
- if (interest.ownerId !== undefined && interest.ownerId !== event.ownerId) {
1232
- this.#resetInterestForOwner(interest, event.ownerId);
1233
- } else {
1234
- interest.ownerId = event.ownerId;
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.objectsById.get(info.id);
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: event.ownerId,
1345
+ ownerId,
1253
1346
  interestId: discovery.interestId
1254
1347
  });
1255
- this.objectsById.set(object.id, 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: event.ownerId
1353
+ ownerId
1261
1354
  });
1262
1355
  }
1263
- interest?.objectsById.set(object.id, 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.objectsById.get(id);
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.id);
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.id);
1341
- this.requestedTypeHashes.delete(object.typeHash);
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.objectsById.get(state.id);
1409
- if (!object || (event.ownerId !== undefined && object.ownerId !== event.ownerId)) {
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.objectsById.get(event.update.objectId);
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.objectsById.get(item.producerId);
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.7",
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
  }