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 +22 -18
- package/README.md +27 -27
- package/bin/node-sen-probe.js +1 -1
- package/index.js +9 -9
- package/lib/client.js +47 -10
- package/lib/sen.js +104 -39
- 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
|
@@ -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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
1752
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
|
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
|
|
1352
|
+
ownerId,
|
|
1341
1353
|
interestId: discovery.interestId
|
|
1342
1354
|
});
|
|
1343
|
-
this.objectsById.set(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
|
|
1360
|
+
ownerId
|
|
1349
1361
|
});
|
|
1350
1362
|
}
|
|
1351
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1429
|
-
this.
|
|
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
|
|
1497
|
-
if (!object || (
|
|
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
|
|
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
|
|
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
|
}
|