sen-ether-client 0.2.1 → 0.2.3
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 +22 -18
- package/README.md +27 -27
- package/bin/node-sen-probe.js +1 -1
- package/index.js +9 -9
- package/lib/client.js +56 -12
- package/lib/sen.js +51 -9
- package/package.json +1 -1
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
|
|
99
|
-
const
|
|
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: '
|
|
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: '
|
|
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
|
|
133
|
-
await
|
|
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 `
|
|
138
|
+
session but publish on a bus such as `domain.bus`:
|
|
139
139
|
|
|
140
140
|
```js
|
|
141
|
-
const
|
|
142
|
-
session: '
|
|
141
|
+
const session = await Sen.connect({
|
|
142
|
+
session: 'session'
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
const objects = await
|
|
146
|
-
bus: '
|
|
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: '
|
|
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
|
|
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
|
|
230
|
-
const object = await interest.waitFor('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
15
|
-
const
|
|
14
|
+
const objects = await sen.interest('SELECT * FROM session.bus');
|
|
15
|
+
const object = await objects.waitFor('object-1');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
object.on('change:label', ({ value }) => {
|
|
18
18
|
console.log('label changed:', value);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
console.log(await
|
|
22
|
-
await
|
|
23
|
-
console.log(await
|
|
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: '
|
|
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: '
|
|
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
|
|
120
|
-
const
|
|
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: '
|
|
130
|
+
// [{ session: 'session', bus: 'bus', qualified: 'session.bus' }]
|
|
131
131
|
|
|
132
|
-
const
|
|
133
|
-
console.log(
|
|
132
|
+
const session = await sen.session('session');
|
|
133
|
+
console.log(session.listBuses());
|
|
134
134
|
|
|
135
|
-
const
|
|
136
|
-
const
|
|
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
|
|
148
|
-
session: '
|
|
147
|
+
const session = await Sen.connect({
|
|
148
|
+
session: 'session'
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
const
|
|
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
|
|
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
|
-
|
|
165
|
+
objects.on('object', object => {
|
|
166
166
|
console.log(object.name, object.className);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
197
|
+
const object = await objects.waitFor('object-1');
|
|
198
198
|
|
|
199
|
-
const
|
|
200
|
-
object => object.className === '
|
|
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
|
|
280
|
+
--bus session.bus
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
## API
|
package/bin/node-sen-probe.js
CHANGED
|
@@ -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
|
|
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
|
|
15
|
-
* console.log(
|
|
14
|
+
* const session = await sen.session('session');
|
|
15
|
+
* console.log(session.listBuses());
|
|
16
16
|
*
|
|
17
|
-
* const
|
|
18
|
-
* const
|
|
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
|
-
*
|
|
22
|
-
* console.log(await
|
|
23
|
-
* await
|
|
24
|
-
* console.log(await
|
|
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
|
@@ -788,6 +788,12 @@ export class EtherClient extends EventEmitter {
|
|
|
788
788
|
this.socket = socket;
|
|
789
789
|
}
|
|
790
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
|
+
});
|
|
791
797
|
socket.on('close', hadError => {
|
|
792
798
|
this.#removeConnection(connection);
|
|
793
799
|
this.emit('connectionClose', { connection, hadError });
|
|
@@ -804,10 +810,18 @@ export class EtherClient extends EventEmitter {
|
|
|
804
810
|
if (connection.processKey) {
|
|
805
811
|
this.connectionsByProcessKey.delete(connection.processKey);
|
|
806
812
|
}
|
|
813
|
+
const leftParticipants = [];
|
|
807
814
|
for (const [busId, participants] of this.remoteParticipantsByBusId) {
|
|
808
815
|
for (const [participantId, participant] of participants) {
|
|
809
816
|
if (participant.connection === connection) {
|
|
810
817
|
participants.delete(participantId);
|
|
818
|
+
leftParticipants.push({
|
|
819
|
+
participantId,
|
|
820
|
+
busId,
|
|
821
|
+
busName: participant.busName,
|
|
822
|
+
connection,
|
|
823
|
+
reason: 'connectionClose'
|
|
824
|
+
});
|
|
811
825
|
}
|
|
812
826
|
}
|
|
813
827
|
if (!participants.size) {
|
|
@@ -824,6 +838,9 @@ export class EtherClient extends EventEmitter {
|
|
|
824
838
|
if (this.socket === connection.socket) {
|
|
825
839
|
this.socket = [...this.connections.values()][0]?.socket;
|
|
826
840
|
}
|
|
841
|
+
for (const participant of leftParticipants) {
|
|
842
|
+
this.emit('busLeft', participant);
|
|
843
|
+
}
|
|
827
844
|
}
|
|
828
845
|
|
|
829
846
|
#configureTcpSocket(socket) {
|
|
@@ -998,7 +1015,7 @@ export class EtherClient extends EventEmitter {
|
|
|
998
1015
|
*/
|
|
999
1016
|
startInterest(bus, query, options = {}) {
|
|
1000
1017
|
const busState = this.#getBus(bus);
|
|
1001
|
-
const id = options.id ?? this.#nextInterestId(busState);
|
|
1018
|
+
const id = options.id ?? this.#nextInterestId(busState, crc32(query));
|
|
1002
1019
|
this.#sendBusControlToRemoteParticipants(busState, {
|
|
1003
1020
|
type: 'InterestStarted',
|
|
1004
1021
|
value: { query, id }
|
|
@@ -1008,7 +1025,13 @@ export class EtherClient extends EventEmitter {
|
|
|
1008
1025
|
return { busName: busState.busName, busId: busState.busId, id, query };
|
|
1009
1026
|
}
|
|
1010
1027
|
|
|
1011
|
-
#nextInterestId(busState) {
|
|
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
|
+
|
|
1012
1035
|
for (let attempts = 0; attempts < 0xffff_ffff; attempts += 1) {
|
|
1013
1036
|
this.nextInterestId = (this.nextInterestId + 1) >>> 0;
|
|
1014
1037
|
const id = this.nextInterestId || 1;
|
|
@@ -1478,7 +1501,27 @@ export class EtherClient extends EventEmitter {
|
|
|
1478
1501
|
const busMessage = decodeBusMessage(frame.message);
|
|
1479
1502
|
this.emit('busFrame', { ...frame, busMessage, remote, multicast: true });
|
|
1480
1503
|
if (busMessage.categoryName === 'controlMessage') {
|
|
1481
|
-
|
|
1504
|
+
const control = busMessage.control;
|
|
1505
|
+
this.emit('busControlMessage', { ...frame, control, remote, multicast: true });
|
|
1506
|
+
switch (control.type) {
|
|
1507
|
+
case 'ObjectsPublished':
|
|
1508
|
+
this.emit('objectsPublished', { bus: busState, remote, multicast: true, ...control.value });
|
|
1509
|
+
break;
|
|
1510
|
+
case 'ObjectsRemoved':
|
|
1511
|
+
this.emit('objectsRemoved', { bus: busState, remote, multicast: true, ...control.value });
|
|
1512
|
+
break;
|
|
1513
|
+
case 'ObjectsStateResponse':
|
|
1514
|
+
this.emit('objectsStateResponse', { bus: busState, remote, multicast: true, ...control.value });
|
|
1515
|
+
break;
|
|
1516
|
+
case 'TypesInfoResponse':
|
|
1517
|
+
this.emit('typesInfoResponse', { bus: busState, remote, multicast: true, ...control.value });
|
|
1518
|
+
break;
|
|
1519
|
+
case 'TypesInfoRejection':
|
|
1520
|
+
this.emit('typesInfoRejection', { bus: busState, remote, multicast: true, ...control.value });
|
|
1521
|
+
break;
|
|
1522
|
+
default:
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1482
1525
|
return;
|
|
1483
1526
|
}
|
|
1484
1527
|
this.emit(busMessage.categoryName, { ...frame, ...busMessage, remote, multicast: true });
|
|
@@ -1746,25 +1789,26 @@ export class EtherClient extends EventEmitter {
|
|
|
1746
1789
|
return;
|
|
1747
1790
|
}
|
|
1748
1791
|
const socket = this.#writableConnectionSocket(connection);
|
|
1792
|
+
if (!socket) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1749
1795
|
const processBusPayload = encodeConfirmedBusFrame({
|
|
1750
1796
|
to: busState.participantId,
|
|
1751
1797
|
busId: busState.busId,
|
|
1752
1798
|
message: busPayload
|
|
1753
1799
|
});
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1800
|
+
try {
|
|
1801
|
+
socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.busMessage, processBusPayload));
|
|
1802
|
+
} catch {
|
|
1803
|
+
this.#removeConnection(connection);
|
|
1804
|
+
}
|
|
1759
1805
|
}
|
|
1760
1806
|
|
|
1761
1807
|
#writableConnectionSocket(connection) {
|
|
1762
1808
|
const socket = connection?.socket;
|
|
1763
1809
|
if (!socket || socket.destroyed || !socket.writable) {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
this.emit('error', error);
|
|
1767
|
-
throw error;
|
|
1810
|
+
this.#removeConnection(connection);
|
|
1811
|
+
return undefined;
|
|
1768
1812
|
}
|
|
1769
1813
|
return socket;
|
|
1770
1814
|
}
|
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));
|
|
@@ -467,6 +468,7 @@ export class Sen extends EventEmitter {
|
|
|
467
468
|
* @param {string} [options.query]
|
|
468
469
|
* @param {boolean} [options.forceBus]
|
|
469
470
|
* @param {number} [options.timeout]
|
|
471
|
+
* @param {number} [options.id] Optional native interest id. Defaults to CRC32(query).
|
|
470
472
|
*/
|
|
471
473
|
async subscribe(busName, options = {}) {
|
|
472
474
|
if (!this.client) {
|
|
@@ -507,7 +509,7 @@ export class Sen extends EventEmitter {
|
|
|
507
509
|
/**
|
|
508
510
|
* Start a native SEN interest and return a live object collection.
|
|
509
511
|
*
|
|
510
|
-
* @param {string} query Native SEN interest query, for example `SELECT * FROM
|
|
512
|
+
* @param {string} query Native SEN interest query, for example `SELECT * FROM session.bus`.
|
|
511
513
|
* @param {object} [options]
|
|
512
514
|
* @param {string} [options.bus] Explicit bus when it cannot be inferred from the query.
|
|
513
515
|
* @param {boolean} [options.forceBus]
|
|
@@ -944,7 +946,7 @@ export class Sen extends EventEmitter {
|
|
|
944
946
|
return this.targetsBySession.keys().next().value;
|
|
945
947
|
}
|
|
946
948
|
|
|
947
|
-
throw new Error(`cannot infer SEN session from bus "${busName}"; use a session-qualified query such as SELECT * FROM
|
|
949
|
+
throw new Error(`cannot infer SEN session from bus "${busName}"; use a session-qualified query such as SELECT * FROM session.${busName}`);
|
|
948
950
|
}
|
|
949
951
|
|
|
950
952
|
#assertBusBelongsToSession(busName, sessionName) {
|
|
@@ -987,6 +989,7 @@ export class Sen extends EventEmitter {
|
|
|
987
989
|
});
|
|
988
990
|
client.on('busLeft', value => {
|
|
989
991
|
this.remoteBuses.delete(value.busName);
|
|
992
|
+
this.#busForEvent(value)?.handleParticipantLeft(value);
|
|
990
993
|
this.emit('busUnavailable', value);
|
|
991
994
|
});
|
|
992
995
|
client.on('objectsPublished', event => this.#busForEvent(event)?.handleObjectsPublished(event));
|
|
@@ -998,6 +1001,10 @@ export class Sen extends EventEmitter {
|
|
|
998
1001
|
client.on('runtimeEvents', event => this.#busForEvent(event)?.handleRuntimeEvents(event));
|
|
999
1002
|
client.on('runtimeMethodResponse', event => this.#busForEvent(event)?.handleRuntimeMethodResponse(event));
|
|
1000
1003
|
client.on('error', error => {
|
|
1004
|
+
if (['EPIPE', 'ECONNRESET', 'SEN_TCP_NOT_WRITABLE'].includes(error?.code)) {
|
|
1005
|
+
this.emit('warning', error);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1001
1008
|
if (this.manualClose || this.reconnecting || this.options.reconnect !== false) {
|
|
1002
1009
|
this.emit('warning', error);
|
|
1003
1010
|
return;
|
|
@@ -1222,6 +1229,7 @@ export class SenBus extends EventEmitter {
|
|
|
1222
1229
|
this.id = id;
|
|
1223
1230
|
this.objectsById = new Map();
|
|
1224
1231
|
this.typeRegistry = new Map();
|
|
1232
|
+
this.typeRegistryByHash = new Map();
|
|
1225
1233
|
this.requestedTypeHashes = new Set();
|
|
1226
1234
|
this.stateRequestedObjectIds = new Set();
|
|
1227
1235
|
this.stateResyncTimers = new Set();
|
|
@@ -1232,7 +1240,7 @@ export class SenBus extends EventEmitter {
|
|
|
1232
1240
|
}
|
|
1233
1241
|
|
|
1234
1242
|
startInterest(query, options = {}) {
|
|
1235
|
-
const started = this.sen.client.startInterest(this.name, query);
|
|
1243
|
+
const started = this.sen.client.startInterest(this.name, query, { id: options.id });
|
|
1236
1244
|
const interest = new SenInterest(this, started.id, query, options);
|
|
1237
1245
|
this.interests.set(interest.id, interest);
|
|
1238
1246
|
return interest;
|
|
@@ -1240,7 +1248,7 @@ export class SenBus extends EventEmitter {
|
|
|
1240
1248
|
|
|
1241
1249
|
stopInterest(id) {
|
|
1242
1250
|
const interestId = typeof id === 'object' ? id.id : id;
|
|
1243
|
-
this.sen.client?.stopInterest(this.name, interestId);
|
|
1251
|
+
this.sen.client?.stopInterest(this.id ?? this.name, interestId);
|
|
1244
1252
|
const interest = this.interests.get(interestId);
|
|
1245
1253
|
this.#detachInterestObjects(interestId, interest);
|
|
1246
1254
|
this.interests.delete(interestId);
|
|
@@ -1264,7 +1272,7 @@ export class SenBus extends EventEmitter {
|
|
|
1264
1272
|
}
|
|
1265
1273
|
}
|
|
1266
1274
|
try {
|
|
1267
|
-
this.sen.client?.leaveBus(this.name);
|
|
1275
|
+
this.sen.client?.leaveBus(this.id ?? this.name);
|
|
1268
1276
|
} catch (error) {
|
|
1269
1277
|
this.sen.emit('warning', error);
|
|
1270
1278
|
}
|
|
@@ -1282,6 +1290,7 @@ export class SenBus extends EventEmitter {
|
|
|
1282
1290
|
}
|
|
1283
1291
|
this.objectsById.clear();
|
|
1284
1292
|
this.typeRegistry.clear();
|
|
1293
|
+
this.typeRegistryByHash.clear();
|
|
1285
1294
|
this.requestedTypeHashes.clear();
|
|
1286
1295
|
this.stateRequestedObjectIds.clear();
|
|
1287
1296
|
this.#clearStateResyncTimers();
|
|
@@ -1307,7 +1316,7 @@ export class SenBus extends EventEmitter {
|
|
|
1307
1316
|
const interests = [...this.interests.values()];
|
|
1308
1317
|
this.interests.clear();
|
|
1309
1318
|
for (const interest of interests) {
|
|
1310
|
-
const started = this.sen.client.startInterest(this.name, interest.query);
|
|
1319
|
+
const started = this.sen.client.startInterest(this.name, interest.query, { id: interest.options.id ?? interest.id });
|
|
1311
1320
|
interest.id = started.id;
|
|
1312
1321
|
interest.resetLocal();
|
|
1313
1322
|
this.interests.set(interest.id, interest);
|
|
@@ -1353,10 +1362,9 @@ export class SenBus extends EventEmitter {
|
|
|
1353
1362
|
ownerId
|
|
1354
1363
|
});
|
|
1355
1364
|
}
|
|
1365
|
+
this.#attachKnownType(object);
|
|
1356
1366
|
interest?.objectsById.set(object.key, object);
|
|
1357
|
-
|
|
1358
|
-
object.applyState(info.state, 'published', info.time, { interestId: discovery.interestId });
|
|
1359
|
-
}
|
|
1367
|
+
object.applyState(info.state?.length ? info.state : Buffer.alloc(0), 'state', info.time, { interestId: discovery.interestId });
|
|
1360
1368
|
if (!this.requestedTypeHashes.has(info.typeHash)) {
|
|
1361
1369
|
this.requestedTypeHashes.add(info.typeHash);
|
|
1362
1370
|
newTypeHashes.add(info.typeHash);
|
|
@@ -1387,6 +1395,19 @@ export class SenBus extends EventEmitter {
|
|
|
1387
1395
|
}
|
|
1388
1396
|
}
|
|
1389
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
|
+
|
|
1390
1411
|
#detachInterestObjects(interestId, interest) {
|
|
1391
1412
|
const normalizedInterestId = interestId >>> 0;
|
|
1392
1413
|
const keyPrefix = `${normalizedInterestId}:`;
|
|
@@ -1478,6 +1499,7 @@ export class SenBus extends EventEmitter {
|
|
|
1478
1499
|
for (const type of event.types ?? []) {
|
|
1479
1500
|
this.typeRegistry.set(type.spec.qualifiedName, type.spec);
|
|
1480
1501
|
if (type.classHash !== undefined) {
|
|
1502
|
+
this.typeRegistryByHash.set(type.classHash >>> 0, type.spec);
|
|
1481
1503
|
for (const object of this.objectsById.values()) {
|
|
1482
1504
|
if (object.typeHash === type.classHash) {
|
|
1483
1505
|
object.spec = type.spec;
|
|
@@ -1674,6 +1696,26 @@ export class SenBus extends EventEmitter {
|
|
|
1674
1696
|
}
|
|
1675
1697
|
}
|
|
1676
1698
|
|
|
1699
|
+
#attachKnownType(object) {
|
|
1700
|
+
if (object.spec) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const specByHash = this.typeRegistryByHash.get(object.typeHash >>> 0);
|
|
1704
|
+
if (specByHash) {
|
|
1705
|
+
object.spec = specByHash;
|
|
1706
|
+
object.emit('type', specByHash);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
for (const spec of this.typeRegistry.values()) {
|
|
1710
|
+
const hash = crc32(spec?.qualifiedName ?? spec?.name ?? '');
|
|
1711
|
+
if (hash === object.typeHash) {
|
|
1712
|
+
object.spec = spec;
|
|
1713
|
+
object.emit('type', spec);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1677
1719
|
#objectByOwnerAndId(ownerId, objectId) {
|
|
1678
1720
|
if (ownerId !== undefined && ownerId !== null) {
|
|
1679
1721
|
return this.objectsById.get(remoteObjectKey(ownerId, objectId));
|