sen-ether-client 0.2.0 → 0.2.2

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 CHANGED
@@ -95,15 +95,15 @@ Preferred multi-session usage:
95
95
  ```js
96
96
  const sen = await Sen.connect();
97
97
 
98
- const hmi = await sen.interest('SELECT * FROM hmi.diagnostics');
99
- const world = await sen.interest('SELECT * FROM world1.environment');
98
+ const first = await sen.interest('SELECT * FROM session.bus');
99
+ const second = await sen.interest('SELECT * FROM otherSession.otherBus');
100
100
  ```
101
101
 
102
102
  TCP discovery hub usage:
103
103
 
104
104
  ```js
105
105
  const sen = await Sen.connect({
106
- session: 'hmi',
106
+ session: 'session',
107
107
  tcpHub: '127.0.0.1:65222'
108
108
  });
109
109
  ```
@@ -116,7 +116,7 @@ Multicast discovery usage:
116
116
 
117
117
  ```js
118
118
  const sen = await Sen.connect({
119
- session: 'hmi',
119
+ session: 'session',
120
120
  interfaceAddress: '127.0.0.1',
121
121
  listenHost: '127.0.0.1',
122
122
  advertisedHost: '127.0.0.1'
@@ -129,21 +129,21 @@ and received on the intended network device.
129
129
  Explicit single-session usage is still supported:
130
130
 
131
131
  ```js
132
- const hmi = await Sen.connect({ session: 'hmi' });
133
- await hmi.interest('SELECT * FROM hmi.diagnostics');
132
+ const session = await Sen.connect({ session: 'session' });
133
+ await session.interest('SELECT * FROM session.bus');
134
134
  ```
135
135
 
136
136
  If a SEN bus name itself contains dots and is not a session-qualified bus, pass
137
137
  it explicitly. This is useful for standalone scenarios that run in one Ether
138
- session but publish on a bus such as `scenario.environment`:
138
+ session but publish on a bus such as `domain.bus`:
139
139
 
140
140
  ```js
141
- const scenario = await Sen.connect({
142
- session: 'scenario'
141
+ const session = await Sen.connect({
142
+ session: 'session'
143
143
  });
144
144
 
145
- const objects = await scenario.interest('SELECT * FROM scenario.environment', {
146
- bus: 'scenario.environment',
145
+ const objects = await session.interest('SELECT * FROM domain.bus', {
146
+ bus: 'domain.bus',
147
147
  forceBus: true
148
148
  });
149
149
  ```
@@ -164,20 +164,24 @@ Main methods:
164
164
  - `await sen.waitForObject(selector, options)`
165
165
  - `await sen.close()`
166
166
 
167
+ By default, interest creation uses the SEN-native `CRC32(query)` value as the
168
+ interest id. Pass `options.id` only when a caller must force a specific native
169
+ interest id.
170
+
167
171
  Session and bus navigation:
168
172
 
169
173
  ```js
170
174
  const sen = await Sen.connect();
171
175
 
172
176
  console.log(await sen.discoverBuses());
173
- // [{ session: 'hmi', bus: 'diagnostics', qualified: 'hmi.diagnostics' }]
177
+ // [{ session: 'session', bus: 'bus', qualified: 'session.bus' }]
174
178
 
175
179
  for (const sessionName of sen.listSessions()) {
176
180
  const session = await sen.session(sessionName);
177
181
  console.log(sessionName, session.listBuses());
178
182
  }
179
183
 
180
- const diagnostics = await sen.session('hmi').then(hmi => hmi.bus('diagnostics'));
184
+ const bus = await sen.session('session').then(session => session.bus('bus'));
181
185
  ```
182
186
 
183
187
  `discoverBuses()` does not create interests and does not join any SEN bus. It
@@ -226,8 +230,8 @@ Main events:
226
230
  Returned by `await sen.interest(query)`.
227
231
 
228
232
  ```js
229
- const interest = await sen.interest('SELECT * FROM hmi.diagnostics');
230
- const object = await interest.waitFor('EtherProbe');
233
+ const interest = await sen.interest('SELECT * FROM session.bus');
234
+ const object = await interest.waitFor('object-1');
231
235
  ```
232
236
 
233
237
  Main methods:
@@ -251,7 +255,7 @@ For browser gateways or high-frequency telemetry, request only the properties
251
255
  you need and emit batches instead of one JS event per property update:
252
256
 
253
257
  ```js
254
- const tracks = await sen.interest('SELECT bus.Track FROM hmi.loadtest', {
258
+ const objects = await sen.interest('SELECT demo.Object FROM session.bus', {
255
259
  properties: ['latitude', 'longitude', 'altitude', 'heading'],
256
260
  changeMode: 'batch',
257
261
  batchIntervalMs: 16,
@@ -261,7 +265,7 @@ const tracks = await sen.interest('SELECT bus.Track FROM hmi.loadtest', {
261
265
  coalesce: true
262
266
  });
263
267
 
264
- tracks.on('changes', ({ changes, dropped }) => {
268
+ objects.on('changes', ({ changes, dropped }) => {
265
269
  // Send one compact WebSocket frame to the browser.
266
270
  });
267
271
  ```
@@ -309,7 +313,7 @@ Main events:
309
313
  64-bit timestamp precision. Convert it explicitly at JSON boundaries:
310
314
 
311
315
  ```js
312
- tracks.on('change', ({ object, name, value, timestampNs }) => {
316
+ objects.on('change', ({ object, name, value, timestampNs }) => {
313
317
  websocket.send(JSON.stringify({
314
318
  object: object.name,
315
319
  name,
package/README.md CHANGED
@@ -11,16 +11,16 @@ import { Sen } from 'sen-ether-client';
11
11
 
12
12
  const sen = await Sen.connect();
13
13
 
14
- const diagnostics = await sen.interest('SELECT * FROM hmi.diagnostics');
15
- const probe = await diagnostics.waitFor('EtherProbe');
14
+ const objects = await sen.interest('SELECT * FROM session.bus');
15
+ const object = await objects.waitFor('object-1');
16
16
 
17
- probe.on('change:label', ({ value }) => {
17
+ object.on('change:label', ({ value }) => {
18
18
  console.log('label changed:', value);
19
19
  });
20
20
 
21
- console.log(await probe.get('label'));
22
- await probe.set('label', 'from-js');
23
- console.log(await probe.call('ping', ['hello']));
21
+ console.log(await object.get('label'));
22
+ await object.set('label', 'from-js');
23
+ console.log(await object.call('ping', ['hello']));
24
24
 
25
25
  await sen.close();
26
26
  ```
@@ -85,7 +85,7 @@ explicitly:
85
85
 
86
86
  ```js
87
87
  const sen = await Sen.connect({
88
- session: 'hmi',
88
+ session: 'session',
89
89
  tcpHub: '127.0.0.1:65222'
90
90
  });
91
91
  ```
@@ -100,7 +100,7 @@ For local multicast tests, select loopback explicitly:
100
100
 
101
101
  ```js
102
102
  const sen = await Sen.connect({
103
- session: 'hmi',
103
+ session: 'session',
104
104
  interfaceAddress: '127.0.0.1',
105
105
  listenHost: '127.0.0.1',
106
106
  advertisedHost: '127.0.0.1'
@@ -116,8 +116,8 @@ configured interests.
116
116
  is inferred from the query:
117
117
 
118
118
  ```js
119
- const hmi = await sen.interest('SELECT * FROM hmi.diagnostics');
120
- const world = await sen.interest('SELECT * FROM world1.environment');
119
+ const first = await sen.interest('SELECT * FROM session.bus');
120
+ const second = await sen.interest('SELECT * FROM otherSession.otherBus');
121
121
  ```
122
122
 
123
123
  You can also navigate explicitly through sessions and buses:
@@ -127,13 +127,13 @@ const sen = await Sen.connect();
127
127
 
128
128
  console.log(sen.listSessions());
129
129
  console.log(await sen.discoverBuses());
130
- // [{ session: 'hmi', bus: 'diagnostics', qualified: 'hmi.diagnostics' }]
130
+ // [{ session: 'session', bus: 'bus', qualified: 'session.bus' }]
131
131
 
132
- const hmi = await sen.session('hmi');
133
- console.log(hmi.listBuses());
132
+ const session = await sen.session('session');
133
+ console.log(session.listBuses());
134
134
 
135
- const diagnostics = await hmi.bus('diagnostics');
136
- const probe = await diagnostics.waitFor('EtherProbe');
135
+ const bus = await session.bus('bus');
136
+ const object = await bus.waitFor('object-1');
137
137
  ```
138
138
 
139
139
  `discoverBuses()` does not create interests and does not join any SEN bus. It
@@ -144,11 +144,11 @@ process connection, it waits up to `busDiscoverySettleMs` milliseconds.
144
144
  You can also connect to one explicit session:
145
145
 
146
146
  ```js
147
- const hmi = await Sen.connect({
148
- session: 'hmi'
147
+ const session = await Sen.connect({
148
+ session: 'session'
149
149
  });
150
150
 
151
- const diagnostics = await hmi.interest('SELECT * FROM hmi.diagnostics');
151
+ const objects = await session.interest('SELECT * FROM session.bus');
152
152
  ```
153
153
 
154
154
  ## Interests
@@ -156,17 +156,17 @@ const diagnostics = await hmi.interest('SELECT * FROM hmi.diagnostics');
156
156
  Create an interest with a normal SEN query:
157
157
 
158
158
  ```js
159
- const tracks = await sen.interest('SELECT * FROM world1.environment');
159
+ const objects = await sen.interest('SELECT * FROM session.bus');
160
160
  ```
161
161
 
162
162
  Listen for objects and changes:
163
163
 
164
164
  ```js
165
- tracks.on('object', object => {
165
+ objects.on('object', object => {
166
166
  console.log(object.name, object.className);
167
167
  });
168
168
 
169
- tracks.on('change', ({ object, name, value }) => {
169
+ objects.on('change', ({ object, name, value }) => {
170
170
  console.log(object.name, name, value);
171
171
  });
172
172
  ```
@@ -175,13 +175,13 @@ For browser gateways or high-frequency telemetry, batch changes and decode only
175
175
  the properties needed by the UI:
176
176
 
177
177
  ```js
178
- const tracks = await sen.interest('SELECT bus.Track FROM hmi.loadtest', {
178
+ const objects = await sen.interest('SELECT demo.Object FROM session.bus', {
179
179
  properties: ['latitude', 'longitude', 'altitude', 'heading'],
180
180
  changeMode: 'batch',
181
181
  coalesce: true
182
182
  });
183
183
 
184
- tracks.on('changes', ({ changes }) => {
184
+ objects.on('changes', ({ changes }) => {
185
185
  websocket.send(JSON.stringify(changes.map(({ object, name, value, timestampNs }) => ({
186
186
  object: object.name,
187
187
  name,
@@ -194,10 +194,10 @@ tracks.on('changes', ({ changes }) => {
194
194
  Get an object by name, id, class name, or predicate:
195
195
 
196
196
  ```js
197
- const aircraft = await tracks.waitFor('blue-air-1');
197
+ const object = await objects.waitFor('object-1');
198
198
 
199
- const firstAircraft = await tracks.waitFor(
200
- object => object.className === 'rpr.Aircraft'
199
+ const firstDemoObject = await objects.waitFor(
200
+ object => object.className === 'demo.Object'
201
201
  );
202
202
  ```
203
203
 
@@ -277,7 +277,7 @@ Probe a bus:
277
277
  ```bash
278
278
  npx sen-ether-probe \
279
279
  --tcp-hub 127.0.0.1:65222 \
280
- --bus hmi.diagnostics
280
+ --bus session.bus
281
281
  ```
282
282
 
283
283
  ## API
@@ -66,7 +66,7 @@ Options:
66
66
  Examples:
67
67
  sen-ether-probe --bus scenario.control
68
68
  sen-ether-probe --tcp-hub 127.0.0.1:64222 --bus scenario.control
69
- sen-ether-probe --bus world1.environment --query "SELECT * FROM world1.environment"
69
+ sen-ether-probe --bus session.bus --query "SELECT * FROM session.bus"
70
70
 
71
71
  Environment:
72
72
  SEN_ETHER_DISCOVERY_PORT Default multicast discovery port
package/index.js CHANGED
@@ -11,17 +11,16 @@
11
11
  * const sen = await Sen.connect();
12
12
  *
13
13
  * console.log(sen.listSessions());
14
- * const hmi = await sen.session('hmi');
15
- * console.log(hmi.listBuses());
14
+ * const session = await sen.session('session');
15
+ * console.log(session.listBuses());
16
16
  *
17
- * const diagnostics = await sen.interest('SELECT * FROM hmi.diagnostics');
18
- * const world = await sen.interest('SELECT * FROM world1.environment');
19
- * const probe = await diagnostics.waitFor('EtherProbe');
17
+ * const objects = await sen.interest('SELECT * FROM session.bus');
18
+ * const object = await objects.waitFor('object-1');
20
19
  *
21
- * probe.on('change:label', ({ value }) => console.log(value));
22
- * console.log(await probe.get('label'));
23
- * await probe.set('label', 'from-js');
24
- * console.log(await probe.call('ping', ['hello']));
20
+ * object.on('change:label', ({ value }) => console.log(value));
21
+ * console.log(await object.get('label'));
22
+ * await object.set('label', 'from-js');
23
+ * console.log(await object.call('ping', ['hello']));
25
24
  *
26
25
  * await sen.close();
27
26
  */
@@ -61,6 +60,7 @@
61
60
  * @property {string} [bus] Explicit bus name when it cannot be inferred from the query.
62
61
  * @property {boolean} [forceBus=false] Join without waiting for the remote process to announce the bus.
63
62
  * @property {number} [timeout] Operation timeout in ms.
63
+ * @property {number} [id] Optional native interest id. Defaults to CRC32(query).
64
64
  * @property {string[]|string} [properties] Optional property names to decode and emit.
65
65
  * @property {'individual'|'batch'|'both'} [changeMode='individual'] Change emission mode.
66
66
  * @property {number} [batchIntervalMs=16] Batched change flush interval in ms.
package/lib/client.js CHANGED
@@ -470,6 +470,7 @@ export class EtherClient extends EventEmitter {
470
470
  this.ready = false;
471
471
  this.buses = new Map();
472
472
  this.remoteParticipantsByBusId = new Map();
473
+ this.nextInterestId = randomUInt32();
473
474
  }
474
475
 
475
476
  /**
@@ -787,6 +788,12 @@ export class EtherClient extends EventEmitter {
787
788
  this.socket = socket;
788
789
  }
789
790
  socket.on('data', chunk => this.#onTcpData(connection, chunk));
791
+ socket.on('error', error => {
792
+ this.#removeConnection(connection);
793
+ if (!['EPIPE', 'ECONNRESET'].includes(error?.code)) {
794
+ this.emit('error', error);
795
+ }
796
+ });
790
797
  socket.on('close', hadError => {
791
798
  this.#removeConnection(connection);
792
799
  this.emit('connectionClose', { connection, hadError });
@@ -803,10 +810,18 @@ export class EtherClient extends EventEmitter {
803
810
  if (connection.processKey) {
804
811
  this.connectionsByProcessKey.delete(connection.processKey);
805
812
  }
813
+ const leftParticipants = [];
806
814
  for (const [busId, participants] of this.remoteParticipantsByBusId) {
807
815
  for (const [participantId, participant] of participants) {
808
816
  if (participant.connection === connection) {
809
817
  participants.delete(participantId);
818
+ leftParticipants.push({
819
+ participantId,
820
+ busId,
821
+ busName: participant.busName,
822
+ connection,
823
+ reason: 'connectionClose'
824
+ });
810
825
  }
811
826
  }
812
827
  if (!participants.size) {
@@ -823,6 +838,9 @@ export class EtherClient extends EventEmitter {
823
838
  if (this.socket === connection.socket) {
824
839
  this.socket = [...this.connections.values()][0]?.socket;
825
840
  }
841
+ for (const participant of leftParticipants) {
842
+ this.emit('busLeft', participant);
843
+ }
826
844
  }
827
845
 
828
846
  #configureTcpSocket(socket) {
@@ -997,7 +1015,7 @@ export class EtherClient extends EventEmitter {
997
1015
  */
998
1016
  startInterest(bus, query, options = {}) {
999
1017
  const busState = this.#getBus(bus);
1000
- const id = options.id ?? crc32(query);
1018
+ const id = options.id ?? this.#nextInterestId(busState, crc32(query));
1001
1019
  this.#sendBusControlToRemoteParticipants(busState, {
1002
1020
  type: 'InterestStarted',
1003
1021
  value: { query, id }
@@ -1007,6 +1025,24 @@ export class EtherClient extends EventEmitter {
1007
1025
  return { busName: busState.busName, busId: busState.busId, id, query };
1008
1026
  }
1009
1027
 
1028
+ #nextInterestId(busState, preferredId) {
1029
+ const preferred = preferredId >>> 0;
1030
+ if (preferred && !busState.interests.has(preferred)) {
1031
+ this.nextInterestId = preferred;
1032
+ return preferred;
1033
+ }
1034
+
1035
+ for (let attempts = 0; attempts < 0xffff_ffff; attempts += 1) {
1036
+ this.nextInterestId = (this.nextInterestId + 1) >>> 0;
1037
+ const id = this.nextInterestId || 1;
1038
+ if (!busState.interests.has(id)) {
1039
+ this.nextInterestId = id;
1040
+ return id;
1041
+ }
1042
+ }
1043
+ throw new Error('could not allocate a SEN interest id');
1044
+ }
1045
+
1010
1046
  #restartInterestForRemote(busState) {
1011
1047
  for (const participant of this.#remoteParticipantsForBus(busState.busId)) {
1012
1048
  for (const interest of busState.interests.values()) {
@@ -1733,25 +1769,26 @@ export class EtherClient extends EventEmitter {
1733
1769
  return;
1734
1770
  }
1735
1771
  const socket = this.#writableConnectionSocket(connection);
1772
+ if (!socket) {
1773
+ return;
1774
+ }
1736
1775
  const processBusPayload = encodeConfirmedBusFrame({
1737
1776
  to: busState.participantId,
1738
1777
  busId: busState.busId,
1739
1778
  message: busPayload
1740
1779
  });
1741
- socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.busMessage, processBusPayload), error => {
1742
- if (error) {
1743
- this.emit('error', error);
1744
- }
1745
- });
1780
+ try {
1781
+ socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.busMessage, processBusPayload));
1782
+ } catch {
1783
+ this.#removeConnection(connection);
1784
+ }
1746
1785
  }
1747
1786
 
1748
1787
  #writableConnectionSocket(connection) {
1749
1788
  const socket = connection?.socket;
1750
1789
  if (!socket || socket.destroyed || !socket.writable) {
1751
- const error = new Error('SEN ether TCP socket is not writable');
1752
- error.code = 'SEN_TCP_NOT_WRITABLE';
1753
- this.emit('error', error);
1754
- throw error;
1790
+ this.#removeConnection(connection);
1791
+ return undefined;
1755
1792
  }
1756
1793
  return socket;
1757
1794
  }
package/lib/sen.js CHANGED
@@ -4,6 +4,7 @@ import { decodePropertyValues, decodeValue, encodeArguments, decodeArguments } f
4
4
  import { EtherClient } from './client.js';
5
5
  import { EtherDiscoveryScanner, TcpDiscoveryHubScanner, scan, scanTcpDiscoveryHub } from './discovery.js';
6
6
  import { methodHash } from './hash32.js';
7
+ import { crc32 } from './crc32.js';
7
8
 
8
9
  function wait(ms) {
9
10
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -161,8 +162,20 @@ function normalizeTimestampNs(value) {
161
162
  return typeof value === 'bigint' ? value : BigInt(value);
162
163
  }
163
164
 
164
- function stateRequestKey(interestId, objectId) {
165
- return `${interestId >>> 0}:${objectId >>> 0}`;
165
+ function stateRequestKey(interestId, ownerId, objectId) {
166
+ return `${interestId >>> 0}:${remoteObjectKey(ownerId, objectId)}`;
167
+ }
168
+
169
+ function remoteObjectKey(ownerId, objectId) {
170
+ const owner = ownerId === undefined || ownerId === null ? 'unknown' : String(ownerId >>> 0);
171
+ return `${owner}:${objectId >>> 0}`;
172
+ }
173
+
174
+ function eventOwnerId(event) {
175
+ if (event?.ownerId !== undefined) {
176
+ return event.ownerId;
177
+ }
178
+ return event?.multicast ? undefined : event?.to;
166
179
  }
167
180
 
168
181
  async function waitForSessionBuses(session, timeoutMs) {
@@ -455,6 +468,7 @@ export class Sen extends EventEmitter {
455
468
  * @param {string} [options.query]
456
469
  * @param {boolean} [options.forceBus]
457
470
  * @param {number} [options.timeout]
471
+ * @param {number} [options.id] Optional native interest id. Defaults to CRC32(query).
458
472
  */
459
473
  async subscribe(busName, options = {}) {
460
474
  if (!this.client) {
@@ -495,7 +509,7 @@ export class Sen extends EventEmitter {
495
509
  /**
496
510
  * Start a native SEN interest and return a live object collection.
497
511
  *
498
- * @param {string} query Native SEN interest query, for example `SELECT * FROM hmi.hud`.
512
+ * @param {string} query Native SEN interest query, for example `SELECT * FROM session.bus`.
499
513
  * @param {object} [options]
500
514
  * @param {string} [options.bus] Explicit bus when it cannot be inferred from the query.
501
515
  * @param {boolean} [options.forceBus]
@@ -932,7 +946,7 @@ export class Sen extends EventEmitter {
932
946
  return this.targetsBySession.keys().next().value;
933
947
  }
934
948
 
935
- throw new Error(`cannot infer SEN session from bus "${busName}"; use a session-qualified query such as SELECT * FROM hmi.${busName}`);
949
+ throw new Error(`cannot infer SEN session from bus "${busName}"; use a session-qualified query such as SELECT * FROM session.${busName}`);
936
950
  }
937
951
 
938
952
  #assertBusBelongsToSession(busName, sessionName) {
@@ -975,6 +989,7 @@ export class Sen extends EventEmitter {
975
989
  });
976
990
  client.on('busLeft', value => {
977
991
  this.remoteBuses.delete(value.busName);
992
+ this.#busForEvent(value)?.handleParticipantLeft(value);
978
993
  this.emit('busUnavailable', value);
979
994
  });
980
995
  client.on('objectsPublished', event => this.#busForEvent(event)?.handleObjectsPublished(event));
@@ -986,6 +1001,10 @@ export class Sen extends EventEmitter {
986
1001
  client.on('runtimeEvents', event => this.#busForEvent(event)?.handleRuntimeEvents(event));
987
1002
  client.on('runtimeMethodResponse', event => this.#busForEvent(event)?.handleRuntimeMethodResponse(event));
988
1003
  client.on('error', error => {
1004
+ if (['EPIPE', 'ECONNRESET', 'SEN_TCP_NOT_WRITABLE'].includes(error?.code)) {
1005
+ this.emit('warning', error);
1006
+ return;
1007
+ }
989
1008
  if (this.manualClose || this.reconnecting || this.options.reconnect !== false) {
990
1009
  this.emit('warning', error);
991
1010
  return;
@@ -1220,7 +1239,7 @@ export class SenBus extends EventEmitter {
1220
1239
  }
1221
1240
 
1222
1241
  startInterest(query, options = {}) {
1223
- const started = this.sen.client.startInterest(this.name, query);
1242
+ const started = this.sen.client.startInterest(this.name, query, { id: options.id });
1224
1243
  const interest = new SenInterest(this, started.id, query, options);
1225
1244
  this.interests.set(interest.id, interest);
1226
1245
  return interest;
@@ -1228,7 +1247,7 @@ export class SenBus extends EventEmitter {
1228
1247
 
1229
1248
  stopInterest(id) {
1230
1249
  const interestId = typeof id === 'object' ? id.id : id;
1231
- this.sen.client?.stopInterest(this.name, interestId);
1250
+ this.sen.client?.stopInterest(this.id ?? this.name, interestId);
1232
1251
  const interest = this.interests.get(interestId);
1233
1252
  this.#detachInterestObjects(interestId, interest);
1234
1253
  this.interests.delete(interestId);
@@ -1252,7 +1271,7 @@ export class SenBus extends EventEmitter {
1252
1271
  }
1253
1272
  }
1254
1273
  try {
1255
- this.sen.client?.leaveBus(this.name);
1274
+ this.sen.client?.leaveBus(this.id ?? this.name);
1256
1275
  } catch (error) {
1257
1276
  this.sen.emit('warning', error);
1258
1277
  }
@@ -1295,7 +1314,7 @@ export class SenBus extends EventEmitter {
1295
1314
  const interests = [...this.interests.values()];
1296
1315
  this.interests.clear();
1297
1316
  for (const interest of interests) {
1298
- const started = this.sen.client.startInterest(this.name, interest.query);
1317
+ const started = this.sen.client.startInterest(this.name, interest.query, { id: interest.options.id ?? interest.id });
1299
1318
  interest.id = started.id;
1300
1319
  interest.resetLocal();
1301
1320
  this.interests.set(interest.id, interest);
@@ -1312,43 +1331,37 @@ export class SenBus extends EventEmitter {
1312
1331
  }
1313
1332
 
1314
1333
  handleObjectsPublished(event) {
1334
+ const ownerId = eventOwnerId(event);
1315
1335
  const newTypeHashes = new Set();
1316
1336
  for (const discovery of event.discoveries ?? []) {
1317
1337
  const interest = this.interests.get(discovery.interestId);
1318
- if (interest && event.ownerId !== undefined) {
1319
- if (interest.ownerId !== undefined && interest.ownerId !== event.ownerId) {
1320
- this.#resetInterestForOwner(interest, event.ownerId);
1321
- } else {
1322
- interest.ownerId = event.ownerId;
1323
- }
1338
+ if (!interest) {
1339
+ continue;
1340
+ }
1341
+ if (ownerId !== undefined) {
1342
+ interest.ownerId ??= ownerId;
1343
+ interest.ownerIds.add(ownerId >>> 0);
1324
1344
  }
1325
1345
 
1326
1346
  for (const info of discovery.objects ?? []) {
1327
- let object = this.objectsById.get(info.id);
1328
- if (object && event.ownerId !== undefined && object.ownerId !== event.ownerId) {
1329
- this.#removeObjectFromAllInterests(object, {
1330
- reason: 'ownerChanged',
1331
- ownerId: event.ownerId,
1332
- previousOwnerId: object.ownerId
1333
- });
1334
- object = undefined;
1335
- }
1347
+ let object = this.#objectByOwnerAndId(ownerId, info.id);
1336
1348
  const isNewObject = !object;
1337
1349
  if (!object) {
1338
1350
  object = new SenRemoteObject(this, {
1339
1351
  ...info,
1340
- ownerId: event.ownerId,
1352
+ ownerId,
1341
1353
  interestId: discovery.interestId
1342
1354
  });
1343
- this.objectsById.set(object.id, object);
1355
+ this.objectsById.set(object.key, object);
1344
1356
  } else {
1345
1357
  object.attachInterest(discovery.interestId);
1346
1358
  object.updateDiscoveryInfo({
1347
1359
  ...info,
1348
- ownerId: event.ownerId
1360
+ ownerId
1349
1361
  });
1350
1362
  }
1351
- interest?.objectsById.set(object.id, object);
1363
+ this.#attachKnownType(object);
1364
+ interest?.objectsById.set(object.key, object);
1352
1365
  if (info.state?.length) {
1353
1366
  object.applyState(info.state, 'published', info.time, { interestId: discovery.interestId });
1354
1367
  }
@@ -1367,10 +1380,14 @@ export class SenBus extends EventEmitter {
1367
1380
  }
1368
1381
 
1369
1382
  handleObjectsRemoved(event) {
1383
+ const ownerId = eventOwnerId(event);
1370
1384
  for (const removal of event.removals ?? []) {
1385
+ const interest = this.interests.get(removal.interestId);
1386
+ if (!interest) {
1387
+ continue;
1388
+ }
1371
1389
  for (const id of removal.ids ?? []) {
1372
- const object = this.objectsById.get(id);
1373
- const interest = this.interests.get(removal.interestId);
1390
+ const object = this.#objectByOwnerAndId(ownerId, id);
1374
1391
  if (object) {
1375
1392
  this.#removeObjectFromInterest(object, removal.interestId, interest);
1376
1393
  }
@@ -1378,6 +1395,19 @@ export class SenBus extends EventEmitter {
1378
1395
  }
1379
1396
  }
1380
1397
 
1398
+ handleParticipantLeft(event) {
1399
+ const ownerId = event?.participantId;
1400
+ if (ownerId === undefined || ownerId === null) {
1401
+ return;
1402
+ }
1403
+ const detail = { reason: event?.reason ?? 'busLeft', ownerId: ownerId >>> 0 };
1404
+ for (const object of [...this.objectsById.values()]) {
1405
+ if (object.ownerId === (ownerId >>> 0)) {
1406
+ this.#removeObjectFromAllInterests(object, detail);
1407
+ }
1408
+ }
1409
+ }
1410
+
1381
1411
  #detachInterestObjects(interestId, interest) {
1382
1412
  const normalizedInterestId = interestId >>> 0;
1383
1413
  const keyPrefix = `${normalizedInterestId}:`;
@@ -1419,14 +1449,16 @@ export class SenBus extends EventEmitter {
1419
1449
 
1420
1450
  #removeObjectFromInterest(object, interestId, interest, detail = {}) {
1421
1451
  const normalizedInterestId = interestId >>> 0;
1422
- this.stateRequestedObjectIds.delete(stateRequestKey(normalizedInterestId, object.id));
1423
- interest?.objectsById.delete(object.id);
1452
+ this.stateRequestedObjectIds.delete(stateRequestKey(normalizedInterestId, object.ownerId, object.id));
1453
+ interest?.objectsById.delete(object.key);
1424
1454
  object.detachInterest(normalizedInterestId);
1425
1455
  object.emit('remove', { interestId: normalizedInterestId, ...detail });
1426
1456
  interest?.emit('remove', object);
1427
1457
  if (object.interestIds.size === 0) {
1428
- this.objectsById.delete(object.id);
1429
- this.requestedTypeHashes.delete(object.typeHash);
1458
+ this.objectsById.delete(object.key);
1459
+ if (![...this.objectsById.values()].some(item => item.typeHash === object.typeHash)) {
1460
+ this.requestedTypeHashes.delete(object.typeHash);
1461
+ }
1430
1462
  this.emit('remove', object);
1431
1463
  this.sen.emit('remove', object);
1432
1464
  }
@@ -1491,10 +1523,15 @@ export class SenBus extends EventEmitter {
1491
1523
  }
1492
1524
 
1493
1525
  handleObjectsStateResponse(event) {
1526
+ const ownerId = eventOwnerId(event);
1494
1527
  for (const response of event.responses ?? []) {
1528
+ const interest = this.interests.get(response.interestId);
1529
+ if (!interest) {
1530
+ continue;
1531
+ }
1495
1532
  for (const state of response.objectStates ?? []) {
1496
- const object = this.objectsById.get(state.id);
1497
- if (!object || (event.ownerId !== undefined && object.ownerId !== event.ownerId)) {
1533
+ const object = this.#objectByOwnerAndId(ownerId, state.id);
1534
+ if (!object || !interest.objectsById.has(object.key)) {
1498
1535
  continue;
1499
1536
  }
1500
1537
  object.applyState(state.state, 'state', state.timestamp, { interestId: response.interestId });
@@ -1503,7 +1540,7 @@ export class SenBus extends EventEmitter {
1503
1540
  }
1504
1541
 
1505
1542
  handleRuntimeObjectUpdate(event) {
1506
- const object = this.objectsById.get(event.update.objectId);
1543
+ const object = this.#objectByOwnerAndId(eventOwnerId(event), event.update.objectId);
1507
1544
  if (!object) {
1508
1545
  return;
1509
1546
  }
@@ -1511,8 +1548,9 @@ export class SenBus extends EventEmitter {
1511
1548
  }
1512
1549
 
1513
1550
  handleRuntimeEvents(event) {
1551
+ const ownerId = eventOwnerId(event);
1514
1552
  for (const item of event.events ?? []) {
1515
- const object = this.objectsById.get(item.producerId);
1553
+ const object = this.#objectByOwnerAndId(ownerId, item.producerId);
1516
1554
  if (!object) {
1517
1555
  continue;
1518
1556
  }
@@ -1583,7 +1621,7 @@ export class SenBus extends EventEmitter {
1583
1621
  if (!object.spec) {
1584
1622
  continue;
1585
1623
  }
1586
- const key = stateRequestKey(interest.id, object.id);
1624
+ const key = stateRequestKey(interest.id, object.ownerId, object.id);
1587
1625
  if (!force && this.stateRequestedObjectIds.has(key)) {
1588
1626
  continue;
1589
1627
  }
@@ -1656,6 +1694,28 @@ export class SenBus extends EventEmitter {
1656
1694
  }
1657
1695
  }
1658
1696
  }
1697
+
1698
+ #attachKnownType(object) {
1699
+ if (object.spec) {
1700
+ return;
1701
+ }
1702
+ for (const spec of this.typeRegistry.values()) {
1703
+ const hash = crc32(spec?.qualifiedName ?? spec?.name ?? '');
1704
+ if (hash === object.typeHash) {
1705
+ object.spec = spec;
1706
+ object.emit('type', spec);
1707
+ return;
1708
+ }
1709
+ }
1710
+ }
1711
+
1712
+ #objectByOwnerAndId(ownerId, objectId) {
1713
+ if (ownerId !== undefined && ownerId !== null) {
1714
+ return this.objectsById.get(remoteObjectKey(ownerId, objectId));
1715
+ }
1716
+ const id = objectId >>> 0;
1717
+ return [...this.objectsById.values()].find(object => object.id === id);
1718
+ }
1659
1719
  }
1660
1720
 
1661
1721
  export class SenInterest extends EventEmitter {
@@ -1665,6 +1725,7 @@ export class SenInterest extends EventEmitter {
1665
1725
  this.id = id;
1666
1726
  this.query = query;
1667
1727
  this.ownerId = undefined;
1728
+ this.ownerIds = new Set();
1668
1729
  this.options = { ...options };
1669
1730
  this.propertyNames = normalizePropertyNames(options.properties ?? options.propertyNames);
1670
1731
  this.changeMode = options.changeMode ?? (options.batch ? 'batch' : 'individual');
@@ -1867,6 +1928,10 @@ export class SenRemoteObject extends EventEmitter {
1867
1928
  return this.propertyTimestamps.get(name);
1868
1929
  }
1869
1930
 
1931
+ get key() {
1932
+ return remoteObjectKey(this.ownerId, this.id);
1933
+ }
1934
+
1870
1935
  isReadyForInterest(interestId) {
1871
1936
  return this.readyInterestIds.has(interestId >>> 0);
1872
1937
  }
@@ -2002,7 +2067,7 @@ export class SenRemoteObject extends EventEmitter {
2002
2067
 
2003
2068
  const interests = [];
2004
2069
  for (const interest of this.bus.interests.values()) {
2005
- if (interest.objectsById.has(this.id)) {
2070
+ if (interest.objectsById.has(this.key) || interest.objectsById.has(this.id)) {
2006
2071
  interests.push(interest);
2007
2072
  }
2008
2073
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sen-ether-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pure JavaScript SEN client for existing kernels over ether",
5
5
  "senCompatibility": {
6
6
  "kernelProtocolVersion": 9,