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/lib/codec.js ADDED
@@ -0,0 +1,501 @@
1
+ import {
2
+ CPU_ARCH,
3
+ ETHER_CONTROL_MESSAGE_KEY,
4
+ ETHER_PROTOCOL_VERSION,
5
+ KERNEL_PROTOCOL_VERSION,
6
+ OS_KIND
7
+ } from './protocol/generated.js';
8
+
9
+ const textDecoder = new TextDecoder();
10
+ const textEncoder = new TextEncoder();
11
+
12
+ export { ETHER_CONTROL_MESSAGE_KEY, ETHER_PROTOCOL_VERSION, KERNEL_PROTOCOL_VERSION };
13
+
14
+ export const PROCESS_MESSAGE_CATEGORY = Object.freeze({
15
+ busMessage: 0,
16
+ controlMessage: 1
17
+ });
18
+
19
+ function enumCode(name, values, label) {
20
+ const index = values.indexOf(name);
21
+ if (index === -1) {
22
+ throw new TypeError(`unknown SEN ${label}: ${name}`);
23
+ }
24
+ return index;
25
+ }
26
+
27
+ function controlTypeFromKey(key) {
28
+ for (const [type, value] of Object.entries(ETHER_CONTROL_MESSAGE_KEY)) {
29
+ if (value === key) {
30
+ return type;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ /**
37
+ * Minimal little-endian reader compatible with SEN InputStream.
38
+ */
39
+ export class SenBinaryReader {
40
+ /**
41
+ * @param {Buffer | Uint8Array | ArrayBuffer} buffer
42
+ */
43
+ constructor(buffer) {
44
+ this.buffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
45
+ this.offset = 0;
46
+ }
47
+
48
+ remaining() {
49
+ return this.buffer.length - this.offset;
50
+ }
51
+
52
+ ensure(size) {
53
+ if (this.remaining() < size) {
54
+ throw new RangeError(`SEN buffer underflow at ${this.offset}; need ${size} bytes`);
55
+ }
56
+ }
57
+
58
+ readUInt8() {
59
+ this.ensure(1);
60
+ const value = this.buffer.readUInt8(this.offset);
61
+ this.offset += 1;
62
+ return value;
63
+ }
64
+
65
+ readUInt16() {
66
+ this.ensure(2);
67
+ const value = this.buffer.readUInt16LE(this.offset);
68
+ this.offset += 2;
69
+ return value;
70
+ }
71
+
72
+ readInt16() {
73
+ this.ensure(2);
74
+ const value = this.buffer.readInt16LE(this.offset);
75
+ this.offset += 2;
76
+ return value;
77
+ }
78
+
79
+ readUInt32() {
80
+ this.ensure(4);
81
+ const value = this.buffer.readUInt32LE(this.offset);
82
+ this.offset += 4;
83
+ return value;
84
+ }
85
+
86
+ readInt32() {
87
+ this.ensure(4);
88
+ const value = this.buffer.readInt32LE(this.offset);
89
+ this.offset += 4;
90
+ return value;
91
+ }
92
+
93
+ readUInt64() {
94
+ this.ensure(8);
95
+ const value = this.buffer.readBigUInt64LE(this.offset);
96
+ this.offset += 8;
97
+ return value;
98
+ }
99
+
100
+ readInt64() {
101
+ this.ensure(8);
102
+ const value = this.buffer.readBigInt64LE(this.offset);
103
+ this.offset += 8;
104
+ return value;
105
+ }
106
+
107
+ readFloat32() {
108
+ this.ensure(4);
109
+ const value = this.buffer.readFloatLE(this.offset);
110
+ this.offset += 4;
111
+ return value;
112
+ }
113
+
114
+ readFloat64() {
115
+ this.ensure(8);
116
+ const value = this.buffer.readDoubleLE(this.offset);
117
+ this.offset += 8;
118
+ return value;
119
+ }
120
+
121
+ readBool() {
122
+ return this.readUInt8() !== 0;
123
+ }
124
+
125
+ readString() {
126
+ const size = this.readUInt32();
127
+ this.ensure(size);
128
+ const value = textDecoder.decode(this.buffer.subarray(this.offset, this.offset + size));
129
+ this.offset += size;
130
+ return value;
131
+ }
132
+
133
+ readBuffer() {
134
+ const size = this.readUInt32();
135
+ this.ensure(size);
136
+ const value = this.buffer.subarray(this.offset, this.offset + size);
137
+ this.offset += size;
138
+ return value;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Minimal little-endian writer compatible with SEN OutputStream.
144
+ */
145
+ export class SenBinaryWriter {
146
+ constructor() {
147
+ this.chunks = [];
148
+ }
149
+
150
+ writeUInt8(value) {
151
+ const buffer = Buffer.allocUnsafe(1);
152
+ buffer.writeUInt8(value);
153
+ this.chunks.push(buffer);
154
+ }
155
+
156
+ writeUInt16(value) {
157
+ const buffer = Buffer.allocUnsafe(2);
158
+ buffer.writeUInt16LE(value);
159
+ this.chunks.push(buffer);
160
+ }
161
+
162
+ writeInt16(value) {
163
+ const buffer = Buffer.allocUnsafe(2);
164
+ buffer.writeInt16LE(value);
165
+ this.chunks.push(buffer);
166
+ }
167
+
168
+ writeUInt32(value) {
169
+ const buffer = Buffer.allocUnsafe(4);
170
+ buffer.writeUInt32LE(value >>> 0);
171
+ this.chunks.push(buffer);
172
+ }
173
+
174
+ writeInt32(value) {
175
+ const buffer = Buffer.allocUnsafe(4);
176
+ buffer.writeInt32LE(value);
177
+ this.chunks.push(buffer);
178
+ }
179
+
180
+ writeUInt64(value) {
181
+ const buffer = Buffer.allocUnsafe(8);
182
+ buffer.writeBigUInt64LE(BigInt(value));
183
+ this.chunks.push(buffer);
184
+ }
185
+
186
+ writeInt64(value) {
187
+ const buffer = Buffer.allocUnsafe(8);
188
+ buffer.writeBigInt64LE(BigInt(value));
189
+ this.chunks.push(buffer);
190
+ }
191
+
192
+ writeFloat32(value) {
193
+ const buffer = Buffer.allocUnsafe(4);
194
+ buffer.writeFloatLE(value);
195
+ this.chunks.push(buffer);
196
+ }
197
+
198
+ writeFloat64(value) {
199
+ const buffer = Buffer.allocUnsafe(8);
200
+ buffer.writeDoubleLE(value);
201
+ this.chunks.push(buffer);
202
+ }
203
+
204
+ writeBool(value) {
205
+ this.writeUInt8(value ? 1 : 0);
206
+ }
207
+
208
+ writeString(value) {
209
+ const encoded = textEncoder.encode(String(value ?? ''));
210
+ this.writeUInt32(encoded.length);
211
+ if (encoded.length !== 0) {
212
+ this.chunks.push(Buffer.from(encoded));
213
+ }
214
+ }
215
+
216
+ writeBuffer(value) {
217
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value ?? []);
218
+ this.writeUInt32(buffer.length);
219
+ if (buffer.length !== 0) {
220
+ this.chunks.push(buffer);
221
+ }
222
+ }
223
+
224
+ toBuffer() {
225
+ return Buffer.concat(this.chunks);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {number} value
231
+ * @returns {string}
232
+ */
233
+ export function uint32ToIpString(value) {
234
+ return [
235
+ (value >>> 24) & 0xff,
236
+ (value >>> 16) & 0xff,
237
+ (value >>> 8) & 0xff,
238
+ value & 0xff
239
+ ].join('.');
240
+ }
241
+
242
+ /**
243
+ * @param {string} value
244
+ * @returns {number}
245
+ */
246
+ export function ipStringToUint32(value) {
247
+ const parts = String(value).split('.').map(Number);
248
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
249
+ throw new TypeError(`invalid IPv4 address: ${value}`);
250
+ }
251
+ return (((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]) >>> 0;
252
+ }
253
+
254
+ export function readProcessInfo(reader) {
255
+ const hostId = reader.readUInt32();
256
+ const processId = reader.readUInt32();
257
+ const sessionId = reader.readUInt32();
258
+ const sessionName = reader.readString();
259
+ const appName = reader.readString();
260
+ const hostName = reader.readString();
261
+ const osKindCode = reader.readUInt8();
262
+ const osName = reader.readString();
263
+ const cpuArchCode = reader.readUInt8();
264
+
265
+ return {
266
+ hostId,
267
+ processId,
268
+ sessionId,
269
+ sessionName,
270
+ appName,
271
+ hostName,
272
+ osKindCode,
273
+ osKind: OS_KIND[osKindCode] ?? `unknown:${osKindCode}`,
274
+ osName,
275
+ cpuArchCode,
276
+ cpuArch: CPU_ARCH[cpuArchCode] ?? `unknown:${cpuArchCode}`
277
+ };
278
+ }
279
+
280
+ export function writeProcessInfo(writer, info) {
281
+ writer.writeUInt32(info.hostId);
282
+ writer.writeUInt32(info.processId);
283
+ writer.writeUInt32(info.sessionId);
284
+ writer.writeString(info.sessionName);
285
+ writer.writeString(info.appName);
286
+ writer.writeString(info.hostName);
287
+ writer.writeUInt8(info.osKindCode ?? enumCode(info.osKind, OS_KIND, 'OsKind'));
288
+ writer.writeString(info.osName);
289
+ writer.writeUInt8(info.cpuArchCode ?? enumCode(info.cpuArch, CPU_ARCH, 'CpuArch'));
290
+ }
291
+
292
+ function readProtocolVersion(reader) {
293
+ return {
294
+ kernel: reader.readUInt32(),
295
+ ether: reader.readUInt32()
296
+ };
297
+ }
298
+
299
+ function writeProtocolVersion(writer, version) {
300
+ writer.writeUInt32(version.kernel);
301
+ writer.writeUInt32(version.ether);
302
+ }
303
+
304
+ function readHello(reader) {
305
+ return {
306
+ info: readProcessInfo(reader),
307
+ udpPort: reader.readUInt16(),
308
+ version: readProtocolVersion(reader)
309
+ };
310
+ }
311
+
312
+ function writeHello(writer, value) {
313
+ writeProcessInfo(writer, value.info);
314
+ writer.writeUInt16(value.udpPort);
315
+ writeProtocolVersion(writer, value.version);
316
+ }
317
+
318
+ function readBusParticipantMessage(reader) {
319
+ return {
320
+ participantId: reader.readUInt32(),
321
+ busId: reader.readUInt32(),
322
+ busName: reader.readString()
323
+ };
324
+ }
325
+
326
+ function writeBusParticipantMessage(writer, value) {
327
+ writer.writeUInt32(value.participantId);
328
+ writer.writeUInt32(value.busId);
329
+ writer.writeString(value.busName);
330
+ }
331
+
332
+ /**
333
+ * Encode sen.components.ether.ControlMessage.
334
+ *
335
+ * Source schema:
336
+ * components/ether/stl/runtime.stl
337
+ *
338
+ * SEN generated serialization writes a u32 alternative key followed by the
339
+ * selected struct payload.
340
+ *
341
+ * @param {{ type: 'Hello' | 'Ready' | 'BusJoined' | 'BusLeft', value?: object } | { Hello: object } | { Ready: object } | { BusJoined: object } | { BusLeft: object }} message
342
+ */
343
+ export function encodeEtherControlMessage(message) {
344
+ const type = message.type ?? Object.keys(message).find(key => key in ETHER_CONTROL_MESSAGE_KEY);
345
+ const value = message.value ?? message[type] ?? {};
346
+
347
+ if (!(type in ETHER_CONTROL_MESSAGE_KEY)) {
348
+ throw new TypeError(`unknown SEN ether ControlMessage: ${type}`);
349
+ }
350
+
351
+ const writer = new SenBinaryWriter();
352
+ writer.writeUInt32(ETHER_CONTROL_MESSAGE_KEY[type]);
353
+
354
+ switch (type) {
355
+ case 'Hello':
356
+ writeHello(writer, value);
357
+ break;
358
+ case 'Ready':
359
+ break;
360
+ case 'BusJoined':
361
+ case 'BusLeft':
362
+ writeBusParticipantMessage(writer, value);
363
+ break;
364
+ default:
365
+ throw new TypeError(`unhandled SEN ether ControlMessage: ${type}`);
366
+ }
367
+
368
+ return writer.toBuffer();
369
+ }
370
+
371
+ /**
372
+ * Decode sen.components.ether.ControlMessage.
373
+ *
374
+ * @param {Buffer | Uint8Array | ArrayBuffer} buffer
375
+ */
376
+ export function decodeEtherControlMessage(buffer) {
377
+ const reader = new SenBinaryReader(buffer);
378
+ const key = reader.readUInt32();
379
+ const type = controlTypeFromKey(key);
380
+
381
+ if (!type) {
382
+ throw new RangeError(`unknown SEN ether ControlMessage key: ${key}`);
383
+ }
384
+
385
+ let value = {};
386
+ switch (type) {
387
+ case 'Hello':
388
+ value = readHello(reader);
389
+ break;
390
+ case 'Ready':
391
+ break;
392
+ case 'BusJoined':
393
+ case 'BusLeft':
394
+ value = readBusParticipantMessage(reader);
395
+ break;
396
+ default:
397
+ throw new TypeError(`unhandled SEN ether ControlMessage: ${type}`);
398
+ }
399
+
400
+ return {
401
+ type,
402
+ value,
403
+ bytesRead: reader.offset
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Encode the 5-byte ProcessHandler TCP frame header plus payload.
409
+ *
410
+ * Source implementation:
411
+ * components/ether/src/process_handler.cpp
412
+ *
413
+ * @param {number} category
414
+ * @param {Buffer | Uint8Array | ArrayBuffer} payload
415
+ */
416
+ export function encodeProcessTcpFrame(category, payload) {
417
+ const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload ?? []);
418
+ const header = Buffer.allocUnsafe(5);
419
+ header.writeUInt8(category, 0);
420
+ header.writeUInt32LE(body.length, 1);
421
+ return Buffer.concat([header, body]);
422
+ }
423
+
424
+ /**
425
+ * @param {Buffer | Uint8Array | ArrayBuffer} buffer
426
+ */
427
+ export function decodeProcessTcpHeader(buffer) {
428
+ const frame = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
429
+ if (frame.length < 5) {
430
+ throw new RangeError(`SEN TCP frame header needs 5 bytes; got ${frame.length}`);
431
+ }
432
+ return {
433
+ category: frame.readUInt8(0),
434
+ payloadSize: frame.readUInt32LE(1)
435
+ };
436
+ }
437
+
438
+ function readEndpoint(reader) {
439
+ const ip = reader.readUInt32();
440
+ const port = reader.readUInt16();
441
+ return {
442
+ ip,
443
+ host: uint32ToIpString(ip),
444
+ port
445
+ };
446
+ }
447
+
448
+ function writeEndpoint(writer, endpoint) {
449
+ writer.writeUInt32(endpoint.ip ?? ipStringToUint32(endpoint.host));
450
+ writer.writeUInt16(endpoint.port);
451
+ }
452
+
453
+ /**
454
+ * Decode sen.components.ether.SessionPresenceBeam.
455
+ *
456
+ * Source schema:
457
+ * components/ether/stl/discovery.stl
458
+ *
459
+ * @param {Buffer | Uint8Array | ArrayBuffer} buffer
460
+ */
461
+ export function decodeSessionPresenceBeam(buffer) {
462
+ const reader = new SenBinaryReader(buffer);
463
+ const protocolVersion = reader.readUInt16();
464
+ const info = readProcessInfo(reader);
465
+ const beamPeriodNs = reader.readInt64();
466
+ const endpointCount = reader.readUInt32();
467
+ const endpoints = [];
468
+
469
+ for (let i = 0; i < endpointCount; i += 1) {
470
+ endpoints.push(readEndpoint(reader));
471
+ }
472
+
473
+ return {
474
+ protocolVersion,
475
+ info,
476
+ beamPeriodNs,
477
+ beamPeriodMs: Number(beamPeriodNs) / 1_000_000,
478
+ endpoints,
479
+ bytesRead: reader.offset
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Encode sen.components.ether.SessionPresenceBeam. Mostly used by tests and
485
+ * future active discovery mode.
486
+ *
487
+ * @param {object} beam
488
+ */
489
+ export function encodeSessionPresenceBeam(beam) {
490
+ const writer = new SenBinaryWriter();
491
+ writer.writeUInt16(beam.protocolVersion);
492
+ writeProcessInfo(writer, beam.info);
493
+ writer.writeInt64(beam.beamPeriodNs);
494
+ writer.writeUInt32(beam.endpoints?.length ?? 0);
495
+
496
+ for (const endpoint of beam.endpoints ?? []) {
497
+ writeEndpoint(writer, endpoint);
498
+ }
499
+
500
+ return writer.toBuffer();
501
+ }
package/lib/crc32.js ADDED
@@ -0,0 +1,26 @@
1
+ const table = new Uint32Array(256);
2
+
3
+ for (let i = 0; i < table.length; i += 1) {
4
+ let value = i;
5
+ for (let bit = 0; bit < 8; bit += 1) {
6
+ value = (value & 1) ? ((value >>> 1) ^ 0xedb88320) : (value >>> 1);
7
+ }
8
+ table[i] = value >>> 0;
9
+ }
10
+
11
+ /**
12
+ * CRC32 compatible with sen::crc32 from sen/core/base/hash32.h.
13
+ *
14
+ * @param {string | Buffer | Uint8Array} input
15
+ * @returns {number}
16
+ */
17
+ export function crc32(input) {
18
+ const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input;
19
+ let checksum = 0xffffffff;
20
+
21
+ for (const byte of bytes) {
22
+ checksum = table[(checksum ^ byte) & 0xff] ^ (checksum >>> 8);
23
+ }
24
+
25
+ return (~checksum) >>> 0;
26
+ }