sen-ether-client 0.1.0
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 +239 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/bin/node-sen-probe.js +426 -0
- package/bin/node-sen-scan.js +77 -0
- package/index.js +75 -0
- package/lib/bus.js +740 -0
- package/lib/client.js +634 -0
- package/lib/codec.js +501 -0
- package/lib/crc32.js +26 -0
- package/lib/discovery.js +439 -0
- package/lib/hash32.js +40 -0
- package/lib/protocol/generated.js +157 -0
- package/lib/sen.js +1346 -0
- package/lib/values.js +421 -0
- package/package.json +31 -0
- package/resources/protocol/ether/discovery.stl +19 -0
- package/resources/protocol/ether/runtime.stl +40 -0
- package/resources/protocol/kernel/basic_types.stl +274 -0
- package/resources/protocol/kernel/bus_protocol.stl +198 -0
- package/resources/protocol/kernel/type_specs.stl +554 -0
- package/resources/protocol/protocol.json +15 -0
- package/scripts/generate-protocol.mjs +111 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import dgram from 'node:dgram';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import {
|
|
8
|
+
decodeBusMessage,
|
|
9
|
+
decodeConfirmedBusFrame,
|
|
10
|
+
encodeBusControlMessage,
|
|
11
|
+
encodeConfirmedBusFrame,
|
|
12
|
+
encodeRuntimeMethodCall
|
|
13
|
+
} from './bus.js';
|
|
14
|
+
import {
|
|
15
|
+
decodeEtherControlMessage,
|
|
16
|
+
decodeProcessTcpHeader,
|
|
17
|
+
encodeEtherControlMessage,
|
|
18
|
+
encodeProcessTcpFrame,
|
|
19
|
+
ETHER_PROTOCOL_VERSION,
|
|
20
|
+
KERNEL_PROTOCOL_VERSION,
|
|
21
|
+
PROCESS_MESSAGE_CATEGORY
|
|
22
|
+
} from './codec.js';
|
|
23
|
+
import { crc32 } from './crc32.js';
|
|
24
|
+
|
|
25
|
+
const LINUX_OS_KIND = 1;
|
|
26
|
+
const X64_CPU_ARCH = 1;
|
|
27
|
+
|
|
28
|
+
function randomUInt32() {
|
|
29
|
+
return randomBytes(4).readUInt32LE(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectOsKind() {
|
|
33
|
+
switch (process.platform) {
|
|
34
|
+
case 'win32':
|
|
35
|
+
return 0;
|
|
36
|
+
case 'linux':
|
|
37
|
+
return 1;
|
|
38
|
+
case 'android':
|
|
39
|
+
return 2;
|
|
40
|
+
case 'darwin':
|
|
41
|
+
return 3;
|
|
42
|
+
default:
|
|
43
|
+
return LINUX_OS_KIND;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function detectCpuArch() {
|
|
48
|
+
switch (process.arch) {
|
|
49
|
+
case 'x64':
|
|
50
|
+
return 1;
|
|
51
|
+
case 'arm64':
|
|
52
|
+
return 12;
|
|
53
|
+
case 'arm':
|
|
54
|
+
return 8;
|
|
55
|
+
default:
|
|
56
|
+
return X64_CPU_ARCH;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a ProcessInfo compatible with sen::kernel::getOwnProcessInfo.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options
|
|
64
|
+
* @param {string} options.sessionName
|
|
65
|
+
* @param {string} [options.appName]
|
|
66
|
+
* @param {string} [options.hostName]
|
|
67
|
+
* @param {number} [options.hostId]
|
|
68
|
+
* @param {number} [options.processId]
|
|
69
|
+
*/
|
|
70
|
+
export function createProcessInfo(options) {
|
|
71
|
+
const hostName = options.hostName ?? os.hostname();
|
|
72
|
+
const sessionName = options.sessionName ?? '';
|
|
73
|
+
return {
|
|
74
|
+
hostId: options.hostId ?? crc32(hostName),
|
|
75
|
+
processId: options.processId ?? randomUInt32(),
|
|
76
|
+
sessionId: crc32(sessionName),
|
|
77
|
+
sessionName,
|
|
78
|
+
appName: options.appName ?? 'sen-ether-client',
|
|
79
|
+
hostName,
|
|
80
|
+
osKindCode: options.osKindCode ?? detectOsKind(),
|
|
81
|
+
osName: options.osName ?? `${os.type()} ${os.release()}`,
|
|
82
|
+
cpuArchCode: options.cpuArchCode ?? detectCpuArch()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateRemoteHello(hello, options, processInfo) {
|
|
87
|
+
if (hello.info.sessionId !== processInfo.sessionId) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`remote SEN session mismatch: expected ${processInfo.sessionName} (${processInfo.sessionId}), ` +
|
|
90
|
+
`got ${hello.info.sessionName} (${hello.info.sessionId})`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (hello.version.kernel !== options.kernelProtocolVersion) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`remote SEN kernel protocol ${hello.version.kernel} is incompatible with ${options.kernelProtocolVersion}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (hello.version.ether !== options.etherProtocolVersion) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`remote SEN ether protocol ${hello.version.ether} is incompatible with ${options.etherProtocolVersion}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Minimal SEN ether process connection.
|
|
109
|
+
*
|
|
110
|
+
* Events:
|
|
111
|
+
* - `remoteProcess`: remote Hello received.
|
|
112
|
+
* - `ready`: remote Ready received.
|
|
113
|
+
* - `controlMessage`: decoded ether ControlMessage.
|
|
114
|
+
* - `busJoined` / `busLeft`: remote process joined/left a bus.
|
|
115
|
+
* - `busFrame`: raw process-level bus frame payload, not decoded yet.
|
|
116
|
+
* - `close`, `error`.
|
|
117
|
+
*/
|
|
118
|
+
export class EtherClient extends EventEmitter {
|
|
119
|
+
/**
|
|
120
|
+
* @param {object} options
|
|
121
|
+
* @param {string} options.sessionName SEN session name. Must match the remote kernel.
|
|
122
|
+
* @param {string} [options.appName]
|
|
123
|
+
* @param {number} [options.kernelProtocolVersion]
|
|
124
|
+
* @param {number} [options.etherProtocolVersion]
|
|
125
|
+
* @param {boolean} [options.socketKeepAlive]
|
|
126
|
+
* @param {number} [options.socketKeepAliveInitialDelayMs]
|
|
127
|
+
* @param {number} [options.socketIdleTimeoutMs]
|
|
128
|
+
*/
|
|
129
|
+
constructor(options) {
|
|
130
|
+
super();
|
|
131
|
+
if (!options?.sessionName) {
|
|
132
|
+
throw new TypeError('EtherClient requires options.sessionName');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.options = {
|
|
136
|
+
appName: 'sen-ether-client',
|
|
137
|
+
kernelProtocolVersion: KERNEL_PROTOCOL_VERSION,
|
|
138
|
+
etherProtocolVersion: ETHER_PROTOCOL_VERSION,
|
|
139
|
+
socketKeepAlive: true,
|
|
140
|
+
socketKeepAliveInitialDelayMs: 1000,
|
|
141
|
+
socketIdleTimeoutMs: 0,
|
|
142
|
+
...options
|
|
143
|
+
};
|
|
144
|
+
this.processInfo = createProcessInfo(this.options);
|
|
145
|
+
this.socket = undefined;
|
|
146
|
+
this.udpSocket = undefined;
|
|
147
|
+
this.receiveBuffer = Buffer.alloc(0);
|
|
148
|
+
this.remoteProcessInfo = undefined;
|
|
149
|
+
this.ready = false;
|
|
150
|
+
this.buses = new Map();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Connect to one endpoint from a SessionPresenceBeam process entry.
|
|
155
|
+
*
|
|
156
|
+
* @param {{ endpoints?: Array<{ host: string, port: number }>, info?: object } | { host: string, port: number }} target
|
|
157
|
+
*/
|
|
158
|
+
async connect(target) {
|
|
159
|
+
const endpoint = target.host ? target : target.endpoints?.[0];
|
|
160
|
+
if (!endpoint) {
|
|
161
|
+
throw new TypeError('SEN ether target must contain host/port or at least one endpoint');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
165
|
+
this.udpSocket.on('message', message => this.#onBusFrame(message));
|
|
166
|
+
this.udpSocket.on('error', error => this.emit('error', error));
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
const onError = error => {
|
|
169
|
+
this.udpSocket?.off('listening', onListening);
|
|
170
|
+
reject(error);
|
|
171
|
+
};
|
|
172
|
+
const onListening = () => {
|
|
173
|
+
this.udpSocket?.off('error', onError);
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
this.udpSocket.once('error', onError);
|
|
177
|
+
this.udpSocket.once('listening', onListening);
|
|
178
|
+
this.udpSocket.bind(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.socket = net.createConnection({ host: endpoint.host, port: endpoint.port });
|
|
182
|
+
this.socket.on('data', chunk => this.#onTcpData(chunk));
|
|
183
|
+
this.socket.on('close', hadError => {
|
|
184
|
+
this.ready = false;
|
|
185
|
+
this.emit('close', hadError);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await new Promise((resolve, reject) => {
|
|
189
|
+
const onError = error => {
|
|
190
|
+
this.socket?.off('connect', onConnect);
|
|
191
|
+
reject(error);
|
|
192
|
+
};
|
|
193
|
+
const onConnect = () => {
|
|
194
|
+
this.socket?.off('error', onError);
|
|
195
|
+
this.socket?.on('error', error => this.emit('error', error));
|
|
196
|
+
this.#configureTcpSocket();
|
|
197
|
+
try {
|
|
198
|
+
this.#sendHello();
|
|
199
|
+
resolve();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
reject(error);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
this.socket.once('error', onError);
|
|
205
|
+
this.socket.once('connect', onConnect);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#configureTcpSocket() {
|
|
212
|
+
if (!this.socket) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (this.options.socketKeepAlive !== false) {
|
|
216
|
+
this.socket.setKeepAlive(true, this.options.socketKeepAliveInitialDelayMs ?? 1000);
|
|
217
|
+
}
|
|
218
|
+
if (this.options.socketIdleTimeoutMs > 0) {
|
|
219
|
+
this.socket.setTimeout(this.options.socketIdleTimeoutMs, () => {
|
|
220
|
+
const error = new Error(`SEN ether TCP socket idle timeout after ${this.options.socketIdleTimeoutMs}ms`);
|
|
221
|
+
error.code = 'SEN_TCP_IDLE_TIMEOUT';
|
|
222
|
+
this.socket?.destroy(error);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async close() {
|
|
228
|
+
const socket = this.socket;
|
|
229
|
+
this.socket = undefined;
|
|
230
|
+
const udpSocket = this.udpSocket;
|
|
231
|
+
this.udpSocket = undefined;
|
|
232
|
+
|
|
233
|
+
const closing = [];
|
|
234
|
+
if (socket && !socket.destroyed) {
|
|
235
|
+
closing.push(new Promise(resolve => {
|
|
236
|
+
socket.once('close', resolve);
|
|
237
|
+
socket.destroy();
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
if (udpSocket) {
|
|
241
|
+
closing.push(new Promise(resolve => {
|
|
242
|
+
udpSocket.close(resolve);
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await Promise.allSettled(closing);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Announce a JS participant on a SEN bus.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} busName
|
|
253
|
+
* @param {{ participantId?: number }} [options]
|
|
254
|
+
*/
|
|
255
|
+
joinBus(busName, options = {}) {
|
|
256
|
+
if (!this.socket) {
|
|
257
|
+
throw new Error('EtherClient is not connected');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const busId = crc32(busName);
|
|
261
|
+
const participantId = options.participantId ?? randomUInt32();
|
|
262
|
+
const bus = {
|
|
263
|
+
busName,
|
|
264
|
+
busId,
|
|
265
|
+
participantId,
|
|
266
|
+
readyRemoteParticipants: new Set(),
|
|
267
|
+
interests: new Map()
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this.buses.set(busId, bus);
|
|
271
|
+
this.#sendControlPayload(encodeEtherControlMessage({
|
|
272
|
+
type: 'BusJoined',
|
|
273
|
+
value: {
|
|
274
|
+
participantId,
|
|
275
|
+
busId,
|
|
276
|
+
busName
|
|
277
|
+
}
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
this.emit('busJoinedLocal', { busName, busId, participantId });
|
|
281
|
+
return { busName, busId, participantId };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Start a SEN object interest on a joined bus.
|
|
286
|
+
*
|
|
287
|
+
* The query syntax is SEN's native Interest query string. Returned object
|
|
288
|
+
* state buffers are intentionally left raw until type-spec decoding is added.
|
|
289
|
+
*
|
|
290
|
+
* @param {string | number} bus Bus name or bus id.
|
|
291
|
+
* @param {string} query
|
|
292
|
+
* @param {{ id?: number }} [options]
|
|
293
|
+
*/
|
|
294
|
+
startInterest(bus, query, options = {}) {
|
|
295
|
+
const busState = this.#getBus(bus);
|
|
296
|
+
const id = options.id ?? crc32(query);
|
|
297
|
+
this.#sendBusControl(busState, busState.participantId, {
|
|
298
|
+
type: 'InterestStarted',
|
|
299
|
+
value: { query, id }
|
|
300
|
+
});
|
|
301
|
+
busState.interests.set(id, { id, query });
|
|
302
|
+
this.emit('interestStarted', { busName: busState.busName, busId: busState.busId, id, query });
|
|
303
|
+
return { busName: busState.busName, busId: busState.busId, id, query };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Stop a previously started interest.
|
|
308
|
+
*
|
|
309
|
+
* @param {string | number} bus Bus name or bus id.
|
|
310
|
+
* @param {number} id
|
|
311
|
+
*/
|
|
312
|
+
stopInterest(bus, id) {
|
|
313
|
+
const busState = this.#getBus(bus);
|
|
314
|
+
busState.interests.delete(id);
|
|
315
|
+
this.#sendBusControl(busState, busState.participantId, {
|
|
316
|
+
type: 'InterestStopped',
|
|
317
|
+
value: { id }
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Request SEN type specs for the given remote object type hashes.
|
|
323
|
+
*
|
|
324
|
+
* @param {string | number} bus Bus name or bus id.
|
|
325
|
+
* @param {Iterable<number>} typeHashes
|
|
326
|
+
*/
|
|
327
|
+
requestTypes(bus, typeHashes) {
|
|
328
|
+
const busState = this.#getBus(bus);
|
|
329
|
+
const requests = [...new Set([...typeHashes].map(value => value >>> 0))];
|
|
330
|
+
if (!requests.length) {
|
|
331
|
+
return { busName: busState.busName, busId: busState.busId, requests };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.#sendBusControl(busState, busState.participantId, {
|
|
335
|
+
type: 'TypesInfoRequest',
|
|
336
|
+
value: {
|
|
337
|
+
ownerId: busState.participantId,
|
|
338
|
+
requests
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
this.emit('typesInfoRequested', { busName: busState.busName, busId: busState.busId, requests });
|
|
342
|
+
return { busName: busState.busName, busId: busState.busId, requests };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Request current dynamic state for already published remote objects.
|
|
347
|
+
*
|
|
348
|
+
* @param {string | number} bus Bus name or bus id.
|
|
349
|
+
* @param {Array<{ interestId: number, objectIds: Array<number> }>} requests
|
|
350
|
+
*/
|
|
351
|
+
requestObjectStates(bus, requests) {
|
|
352
|
+
const busState = this.#getBus(bus);
|
|
353
|
+
const normalized = requests
|
|
354
|
+
.map(request => ({
|
|
355
|
+
interestId: request.interestId >>> 0,
|
|
356
|
+
objectIds: [...new Set((request.objectIds ?? []).map(value => value >>> 0))]
|
|
357
|
+
}))
|
|
358
|
+
.filter(request => request.objectIds.length);
|
|
359
|
+
|
|
360
|
+
if (!normalized.length) {
|
|
361
|
+
return { busName: busState.busName, busId: busState.busId, requests: normalized };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.#sendBusControl(busState, busState.participantId, {
|
|
365
|
+
type: 'ObjectsStateRequest',
|
|
366
|
+
value: {
|
|
367
|
+
ownerId: busState.participantId,
|
|
368
|
+
requests: normalized
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
this.emit('objectsStateRequested', { busName: busState.busName, busId: busState.busId, requests: normalized });
|
|
372
|
+
return { busName: busState.busName, busId: busState.busId, requests: normalized };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Send a runtime method call to a remote participant on a joined bus.
|
|
377
|
+
*
|
|
378
|
+
* @param {string | number} bus Bus name or bus id.
|
|
379
|
+
* @param {object} call
|
|
380
|
+
* @param {number} call.to Remote participant/object owner id.
|
|
381
|
+
* @param {number} call.objectId Remote object id.
|
|
382
|
+
* @param {number} call.methodId SEN method member hash.
|
|
383
|
+
* @param {number} call.ticketId Local call id.
|
|
384
|
+
* @param {boolean} [call.confirmed]
|
|
385
|
+
* @param {Buffer | Uint8Array | ArrayBuffer} [call.argumentsBuffer]
|
|
386
|
+
*/
|
|
387
|
+
sendRuntimeMethodCall(bus, call) {
|
|
388
|
+
const busState = this.#getBus(bus);
|
|
389
|
+
const message = encodeRuntimeMethodCall({
|
|
390
|
+
ownerId: busState.participantId,
|
|
391
|
+
objectId: call.objectId,
|
|
392
|
+
methodId: call.methodId,
|
|
393
|
+
ticketId: call.ticketId,
|
|
394
|
+
confirmed: call.confirmed,
|
|
395
|
+
argumentsBuffer: call.argumentsBuffer
|
|
396
|
+
});
|
|
397
|
+
this.#sendBusMessage(busState, call.to, message);
|
|
398
|
+
this.emit('runtimeMethodCallSent', {
|
|
399
|
+
busName: busState.busName,
|
|
400
|
+
busId: busState.busId,
|
|
401
|
+
to: call.to,
|
|
402
|
+
objectId: call.objectId,
|
|
403
|
+
methodId: call.methodId,
|
|
404
|
+
ticketId: call.ticketId,
|
|
405
|
+
confirmed: Boolean(call.confirmed)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Announce that the local JS participant leaves a SEN bus.
|
|
411
|
+
*
|
|
412
|
+
* @param {string | number} bus Bus name or bus id.
|
|
413
|
+
*/
|
|
414
|
+
leaveBus(bus) {
|
|
415
|
+
const busState = this.#getBus(bus);
|
|
416
|
+
for (const id of Array.from(busState.interests.keys())) {
|
|
417
|
+
this.stopInterest(busState.busId, id);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this.#sendControlPayload(encodeEtherControlMessage({
|
|
421
|
+
type: 'BusLeft',
|
|
422
|
+
value: {
|
|
423
|
+
participantId: busState.participantId,
|
|
424
|
+
busId: busState.busId,
|
|
425
|
+
busName: busState.busName
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
this.buses.delete(busState.busId);
|
|
429
|
+
this.emit('busLeftLocal', {
|
|
430
|
+
busName: busState.busName,
|
|
431
|
+
busId: busState.busId,
|
|
432
|
+
participantId: busState.participantId
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#sendHello() {
|
|
437
|
+
const udpPort = this.udpSocket?.address()?.port;
|
|
438
|
+
const payload = encodeEtherControlMessage({
|
|
439
|
+
type: 'Hello',
|
|
440
|
+
value: {
|
|
441
|
+
info: this.processInfo,
|
|
442
|
+
udpPort,
|
|
443
|
+
version: {
|
|
444
|
+
kernel: this.options.kernelProtocolVersion,
|
|
445
|
+
ether: this.options.etherProtocolVersion
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
this.#sendControlPayload(payload);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#sendReady() {
|
|
453
|
+
this.#sendControlPayload(encodeEtherControlMessage({ type: 'Ready' }));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#sendControlPayload(payload) {
|
|
457
|
+
const socket = this.#writableSocket();
|
|
458
|
+
socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.controlMessage, payload), error => {
|
|
459
|
+
if (error) {
|
|
460
|
+
this.emit('error', error);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#onTcpData(chunk) {
|
|
466
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
467
|
+
|
|
468
|
+
while (this.receiveBuffer.length >= 5) {
|
|
469
|
+
const header = decodeProcessTcpHeader(this.receiveBuffer);
|
|
470
|
+
const frameSize = 5 + header.payloadSize;
|
|
471
|
+
if (this.receiveBuffer.length < frameSize) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const payload = this.receiveBuffer.subarray(5, frameSize);
|
|
476
|
+
this.receiveBuffer = this.receiveBuffer.subarray(frameSize);
|
|
477
|
+
this.#onFrame(header.category, payload);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
#onFrame(category, payload) {
|
|
482
|
+
if (category === PROCESS_MESSAGE_CATEGORY.controlMessage) {
|
|
483
|
+
const message = decodeEtherControlMessage(payload);
|
|
484
|
+
this.emit('controlMessage', message);
|
|
485
|
+
this.#onControlMessage(message);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (category === PROCESS_MESSAGE_CATEGORY.busMessage) {
|
|
490
|
+
this.#onBusFrame(payload);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
this.emit('error', new RangeError(`unknown SEN process frame category: ${category}`));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#onControlMessage(message) {
|
|
498
|
+
switch (message.type) {
|
|
499
|
+
case 'Hello':
|
|
500
|
+
this.#onHello(message.value);
|
|
501
|
+
break;
|
|
502
|
+
case 'Ready':
|
|
503
|
+
this.ready = true;
|
|
504
|
+
this.emit('ready', this.remoteProcessInfo);
|
|
505
|
+
break;
|
|
506
|
+
case 'BusJoined':
|
|
507
|
+
this.emit('busJoined', message.value);
|
|
508
|
+
break;
|
|
509
|
+
case 'BusLeft':
|
|
510
|
+
this.emit('busLeft', message.value);
|
|
511
|
+
break;
|
|
512
|
+
default:
|
|
513
|
+
this.emit('error', new RangeError(`unknown SEN ether control message: ${message.type}`));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#onHello(hello) {
|
|
518
|
+
try {
|
|
519
|
+
validateRemoteHello(hello, this.options, this.processInfo);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
this.socket?.destroy(error);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.remoteProcessInfo = hello.info;
|
|
526
|
+
this.emit('remoteProcess', hello);
|
|
527
|
+
try {
|
|
528
|
+
this.#sendReady();
|
|
529
|
+
} catch (error) {
|
|
530
|
+
this.emit('error', error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#onBusFrame(payload) {
|
|
535
|
+
const frame = decodeConfirmedBusFrame(payload);
|
|
536
|
+
const busMessage = decodeBusMessage(frame.message);
|
|
537
|
+
this.emit('busFrame', { ...frame, busMessage });
|
|
538
|
+
|
|
539
|
+
if (busMessage.categoryName !== 'controlMessage') {
|
|
540
|
+
this.emit(busMessage.categoryName, { ...frame, ...busMessage });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const busState = this.buses.get(frame.busId);
|
|
545
|
+
const control = busMessage.control;
|
|
546
|
+
this.emit('busControlMessage', { ...frame, control });
|
|
547
|
+
|
|
548
|
+
if (!busState) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
switch (control.type) {
|
|
553
|
+
case 'RemoteParticipantReady':
|
|
554
|
+
this.#onRemoteParticipantReady(busState, frame, control.value);
|
|
555
|
+
break;
|
|
556
|
+
case 'ObjectsPublished':
|
|
557
|
+
this.emit('objectsPublished', { bus: busState, ...control.value });
|
|
558
|
+
break;
|
|
559
|
+
case 'ObjectsRemoved':
|
|
560
|
+
this.emit('objectsRemoved', { bus: busState, ...control.value });
|
|
561
|
+
break;
|
|
562
|
+
case 'ObjectsStateResponse':
|
|
563
|
+
this.emit('objectsStateResponse', { bus: busState, ...control.value });
|
|
564
|
+
break;
|
|
565
|
+
case 'TypesInfoResponse':
|
|
566
|
+
this.emit('typesInfoResponse', { bus: busState, ...control.value });
|
|
567
|
+
break;
|
|
568
|
+
case 'TypesInfoRejection':
|
|
569
|
+
this.emit('typesInfoRejection', { bus: busState, ...control.value });
|
|
570
|
+
break;
|
|
571
|
+
default:
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#onRemoteParticipantReady(busState, frame, value) {
|
|
577
|
+
if (value.id !== busState.participantId) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const remoteParticipantId = frame.to;
|
|
582
|
+
if (!busState.readyRemoteParticipants.has(remoteParticipantId)) {
|
|
583
|
+
busState.readyRemoteParticipants.add(remoteParticipantId);
|
|
584
|
+
this.#sendBusControl(busState, busState.participantId, {
|
|
585
|
+
type: 'RemoteParticipantReady',
|
|
586
|
+
value: { id: remoteParticipantId }
|
|
587
|
+
});
|
|
588
|
+
this.emit('busParticipantReady', {
|
|
589
|
+
busName: busState.busName,
|
|
590
|
+
busId: busState.busId,
|
|
591
|
+
participantId: busState.participantId,
|
|
592
|
+
remoteParticipantId
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#sendBusControl(busState, to, message) {
|
|
598
|
+
const busPayload = encodeBusControlMessage(message);
|
|
599
|
+
this.#sendBusMessage(busState, to, busPayload);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#sendBusMessage(busState, to, busPayload) {
|
|
603
|
+
const socket = this.#writableSocket();
|
|
604
|
+
const processBusPayload = encodeConfirmedBusFrame({
|
|
605
|
+
to,
|
|
606
|
+
busId: busState.busId,
|
|
607
|
+
message: busPayload
|
|
608
|
+
});
|
|
609
|
+
socket.write(encodeProcessTcpFrame(PROCESS_MESSAGE_CATEGORY.busMessage, processBusPayload), error => {
|
|
610
|
+
if (error) {
|
|
611
|
+
this.emit('error', error);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#writableSocket() {
|
|
617
|
+
if (!this.socket || this.socket.destroyed || !this.socket.writable) {
|
|
618
|
+
const error = new Error('SEN ether TCP socket is not writable');
|
|
619
|
+
error.code = 'SEN_TCP_NOT_WRITABLE';
|
|
620
|
+
this.emit('error', error);
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
return this.socket;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#getBus(bus) {
|
|
627
|
+
const busId = typeof bus === 'number' ? bus : crc32(bus);
|
|
628
|
+
const busState = this.buses.get(busId);
|
|
629
|
+
if (!busState) {
|
|
630
|
+
throw new Error(`SEN bus is not joined: ${bus}`);
|
|
631
|
+
}
|
|
632
|
+
return busState;
|
|
633
|
+
}
|
|
634
|
+
}
|