layout-data 0.0.0-stage → 0.3.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/src/Ldata.js ADDED
@@ -0,0 +1,541 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { EventEmitter } from "node:events";
3
+ import { Ltag } from "./Ltag.js";
4
+ import { emit_all } from "./dispatch.js";
5
+ import { check_endian, io_range, load_entry, store_entry } from "./endian.js";
6
+ import { Observer } from "./observer.js";
7
+ import { compiled_of, find_layout, get_layout } from "./registry.js";
8
+ import { is_reserved_name } from "./reserved.js";
9
+
10
+ /** @import { Class_Access_Listener, Data_Listener, Endian, Layout } from "./typedefs.js" */
11
+ /** @import { Compiled_Layout, Layout_Entry } from "./internal_types.js" */
12
+
13
+ /** The classes built so far, by layout name (doc/design.md §4). */
14
+ const classes = new Map();
15
+
16
+ /**
17
+ * The class-level access listeners (doc/spec-data-api.md §2.8). They are kept
18
+ * here and not on the data blocks, so that nothing done to a data block
19
+ * removes them.
20
+ *
21
+ * @type {Set<Class_Access_Listener>}
22
+ */
23
+ const class_access_listeners = new Set();
24
+
25
+ /** Set while an access listener runs: the accesses made meanwhile emit nothing. */
26
+ let in_access = false;
27
+
28
+ /**
29
+ * The registered layout an Ldata argument stands for.
30
+ *
31
+ * @param {Layout | string} layout - A layout name, or the registered layout of its name.
32
+ * @returns {Layout}
33
+ * @throws {Error} For an unknown name, or a layout that is not the registered one.
34
+ */
35
+ function resolve_layout(layout) {
36
+ if (typeof layout === "string") return get_layout(layout);
37
+ if (find_layout(layout?.name) !== layout) throw new Error(`${layout?.name} is not a registered layout`);
38
+ return layout;
39
+ }
40
+
41
+ export class Ldata extends EventEmitter {
42
+ /**
43
+ * @type {Buffer}
44
+ * @description Internal buffer, private (doc/spec-data-api.md §2.2).
45
+ * All data items, including combined words, are stored in Big-Endian format.
46
+ * Device endianness is converted only at the IO boundary (the IO layer).
47
+ */
48
+ #buffer;
49
+ /** @returns {number} Size of the data block in bytes. */
50
+ get size() {
51
+ return this.#buffer.length;
52
+ }
53
+
54
+ /** @type {string} */
55
+ #name;
56
+ /** @returns {string} The name given at creation, used in logs and error messages. */
57
+ get name() {
58
+ return this.#name;
59
+ }
60
+
61
+ /** @type {Layout} */
62
+ #layout;
63
+ /** @type {Compiled_Layout} */
64
+ #compiled;
65
+ /**
66
+ * The compiled layout of this data block (doc/design.md §3), read-only.
67
+ * The tag accessors are defined once on the prototype of the class built for the layout.
68
+ *
69
+ * @returns {Layout}
70
+ */
71
+ get layout() { return this.#layout; }
72
+
73
+ /** @type {Observer} */
74
+ #observer;
75
+ /**
76
+ * The open transaction, null outside one (doc/spec-data-api.md §4.2): whether
77
+ * it wrote, and the tags it read and wrote, gathered for `access`.
78
+ *
79
+ * @type {{ written: boolean, read: Set<Ltag>, write: Set<Ltag> } | null}
80
+ */
81
+ #transaction = null;
82
+ /**
83
+ * @type {Map<string, Ltag>}
84
+ * @description The tag handles created so far, by tag name.
85
+ */
86
+ #handles = new Map();
87
+ /**
88
+ * Subscribes a listener; typed for `change` and `write`, any listener for
89
+ * another event name (doc/spec-data-api.md §2.7).
90
+ *
91
+ * @template {string | symbol} E
92
+ * @param {E} event
93
+ * @param {Data_Listener<E>} listener
94
+ * @returns {this}
95
+ */
96
+ on(event, listener) { return super.on(event, listener); }
97
+
98
+ /**
99
+ * Subscribes a listener for one emission, typed like `on`.
100
+ *
101
+ * @template {string | symbol} E
102
+ * @param {E} event
103
+ * @param {Data_Listener<E>} listener
104
+ * @returns {this}
105
+ */
106
+ once(event, listener) { return super.once(event, listener); }
107
+
108
+ /**
109
+ * Removes a listener, typed like `on`.
110
+ *
111
+ * @template {string | symbol} E
112
+ * @param {E} event
113
+ * @param {Data_Listener<E>} listener
114
+ * @returns {this}
115
+ */
116
+ off(event, listener) { return super.off(event, listener); }
117
+
118
+ /**
119
+ * Retrieves the handle of a tag, creating it on first use.
120
+ * @param {string} tagname - The name of the tag to retrieve.
121
+ * @returns {Ltag|undefined} The Ltag handle, undefined for an unknown name.
122
+ */
123
+ get(tagname) {
124
+ let tag = this.#handles.get(tagname);
125
+ if (tag) return tag;
126
+ const entry = this.#compiled.entries.get(tagname);
127
+ if (!entry) return undefined;
128
+ tag = new Ltag(this, entry);
129
+ this.#handles.set(tagname, tag);
130
+ return tag;
131
+ }
132
+
133
+ /**
134
+ * Runs fn synchronously and makes its accesses and writes one whole: no
135
+ * event is emitted during it; when fn returns, the gathered `access` is
136
+ * emitted, one detection runs and one `write` is emitted if fn wrote; when
137
+ * fn throws, the buffer is restored and only the gathered `access` of kind
138
+ * `read` is emitted (doc/spec-data-api.md §4.2). The return value of fn is
139
+ * not inspected: a Promise is neither awaited nor rejected.
140
+ *
141
+ * @template T
142
+ * @param {() => T} fn
143
+ * @returns {T} The return value of fn.
144
+ * @throws {Error} Inside a transaction; or whatever fn throws, after the rollback.
145
+ */
146
+ transaction(fn) {
147
+ if (this.#transaction) throw new Error(`${this.name}: a transaction is already open`);
148
+ const saved = Buffer.from(this.#buffer);
149
+ const state = { written: false, read: new Set(), write: new Set() };
150
+ this.#transaction = state;
151
+ let result;
152
+ try {
153
+ result = fn();
154
+ } catch (error) {
155
+ this.#transaction = null;
156
+ saved.copy(this.#buffer);
157
+ this.#observer.discard();
158
+ this.#emit_access("read", this.#in_layout_order(state.read), []); // the writes were undone, the reads were not
159
+ throw error;
160
+ }
161
+ this.#transaction = null;
162
+ const errors = [];
163
+ this.#emit_access("read", this.#in_layout_order(state.read), errors);
164
+ this.#emit_access("write", this.#in_layout_order(state.write), errors);
165
+ this.#observer.diff(errors);
166
+ if (state.written) emit_all(this, "write", [this], errors);
167
+ if (errors.length > 0) throw errors[0];
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Reads a range of bytes (doc/spec-data-api.md §2.5).
173
+ *
174
+ * @param {number} [offset=0] - Byte offset.
175
+ * @param {number} [length] - Number of bytes, up to the end of the data block by default.
176
+ * @returns {Buffer} A copy: changing it does not affect the data block.
177
+ * @throws {RangeError} For an offset or length out of range.
178
+ */
179
+ read_bytes(offset = 0, length = this.#buffer.length - offset) {
180
+ this.#check_range(offset, length);
181
+ const bytes = Buffer.from(this.#buffer.subarray(offset, offset + length));
182
+ if (length > 0 && this.#has_access_listeners()) this.#accessed("read", this.#tags_in(offset, offset + length));
183
+ return bytes;
184
+ }
185
+
186
+ /**
187
+ * Writes all the bytes of source from a byte offset (doc/spec-data-api.md §2.5).
188
+ * An empty source writes nothing and emits no `write`.
189
+ *
190
+ * @param {number} offset - Byte offset.
191
+ * @param {Uint8Array} source
192
+ * @throws {RangeError} For an offset out of range, or a run past the end of the data block.
193
+ * @throws {TypeError} When source is not a Uint8Array.
194
+ */
195
+ write_bytes(offset, source) {
196
+ if (!(source instanceof Uint8Array)) throw new TypeError(`${this.name}: source must be a Uint8Array`);
197
+ this.#check_range(offset, source.length);
198
+ if (source.length === 0) return; // nothing written: not a write
199
+ this.#buffer.set(source, offset);
200
+ this.#written(offset, offset + source.length);
201
+ }
202
+
203
+ /**
204
+ * Reads the bits selected by mask of the byte at offset
205
+ * (doc/spec-data-api.md §2.5); `access` names only the Bools of those bits.
206
+ *
207
+ * @param {number} offset - Byte offset.
208
+ * @param {number} mask - An integer from 1 to 0xff.
209
+ * @returns {boolean} Whether any of the bits is set.
210
+ * @throws {RangeError} For an offset out of range, or a mask that is not an integer from 1 to 0xff.
211
+ */
212
+ read_bit(offset, mask) {
213
+ this.#check_bit(offset, mask);
214
+ const value = (this.#buffer[offset] & mask) !== 0;
215
+ if (this.#has_access_listeners()) this.#accessed("read", this.#tags_in(offset, offset + 1, mask));
216
+ return value;
217
+ }
218
+
219
+ /**
220
+ * Writes the bits selected by mask of the byte at offset; the other bits
221
+ * are unchanged (doc/spec-data-api.md §2.5).
222
+ *
223
+ * @param {number} offset - Byte offset.
224
+ * @param {number} mask - An integer from 1 to 0xff.
225
+ * @param {boolean} value - Taken by truthiness.
226
+ * @throws {RangeError} For an offset out of range, or a mask that is not an integer from 1 to 0xff.
227
+ */
228
+ write_bit(offset, mask, value) {
229
+ this.#check_bit(offset, mask);
230
+ const buffer = this.#buffer;
231
+ buffer[offset] = value ? buffer[offset] | mask : buffer[offset] & ~mask;
232
+ this.#written(offset, offset + 1, null, mask);
233
+ }
234
+
235
+ /**
236
+ * Converts the bytes of a tag from a device-ordered IO buffer and writes
237
+ * them into the data block; a write like any other
238
+ * (doc/spec-data-api.md §2.9).
239
+ *
240
+ * @param {string} name - The tag name.
241
+ * @param {Uint8Array} io_buffer - Indexed like the byte offsets of the data block.
242
+ * @param {Endian} endian - Device byte order.
243
+ * @throws {Error} For an unknown tag name.
244
+ * @throws {RangeError} For another byte order, or a tag out of the IO buffer.
245
+ * @throws {TypeError} When io_buffer is not a Uint8Array.
246
+ */
247
+ load_tag(name, io_buffer, endian) {
248
+ const entry = this.#entry_for_io(name, io_buffer, endian);
249
+ load_entry(entry, io_buffer, this.#buffer, endian);
250
+ this.#written(entry.byte, entry.byte + entry.size, entry);
251
+ }
252
+
253
+ /**
254
+ * Converts the bytes of a tag and writes them into a device-ordered IO
255
+ * buffer; the data block is unchanged and no event is emitted
256
+ * (doc/spec-data-api.md §2.9).
257
+ *
258
+ * @param {string} name - The tag name.
259
+ * @param {Uint8Array} io_buffer - Indexed like the byte offsets of the data block.
260
+ * @param {Endian} endian - Device byte order.
261
+ * @throws {Error} For an unknown tag name.
262
+ * @throws {RangeError} For another byte order, or a tag out of the IO buffer.
263
+ * @throws {TypeError} When io_buffer is not a Uint8Array.
264
+ */
265
+ store_tag(name, io_buffer, endian) {
266
+ const entry = this.#entry_for_io(name, io_buffer, endian);
267
+ store_entry(entry, this.#buffer, io_buffer, endian);
268
+ if (this.#has_access_listeners()) this.#accessed("read", [this.get(name)]);
269
+ }
270
+
271
+ /**
272
+ * The compiled entry of a tag, after checking the arguments of load_tag
273
+ * and store_tag before anything is changed.
274
+ *
275
+ * @param {string} name
276
+ * @param {Uint8Array} io_buffer
277
+ * @param {Endian} endian
278
+ * @returns {import("./internal_types.js").Layout_Entry}
279
+ */
280
+ #entry_for_io(name, io_buffer, endian) {
281
+ if (!(io_buffer instanceof Uint8Array)) throw new TypeError(`${this.name}: io_buffer must be a Uint8Array`);
282
+ check_endian(endian);
283
+ const entry = this.#compiled.entries.get(name);
284
+ if (!entry) throw new Error(`${this.name}: unknown tag ${name}`);
285
+ if (io_range(entry, endian).end > io_buffer.length) {
286
+ throw new RangeError(`${this.name}.${name}: out of the io_buffer of ${io_buffer.length} bytes`);
287
+ }
288
+ return entry;
289
+ }
290
+
291
+ /**
292
+ * Checks a byte range before anything is read or written.
293
+ *
294
+ * @param {number} offset
295
+ * @param {number} length
296
+ * @throws {RangeError}
297
+ */
298
+ #check_range(offset, length) {
299
+ if (!Number.isInteger(offset) || offset < 0) {
300
+ throw new RangeError(`${this.name}: offset ${offset} must be a non-negative integer`);
301
+ }
302
+ if (!Number.isInteger(length) || length < 0 || offset + length > this.#buffer.length) {
303
+ throw new RangeError(`${this.name}: length ${length} at offset ${offset} exceeds the data block`);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Checks the arguments of read_bit and write_bit.
309
+ *
310
+ * @param {number} offset
311
+ * @param {number} mask
312
+ * @throws {RangeError}
313
+ */
314
+ #check_bit(offset, mask) {
315
+ this.#check_range(offset, 1);
316
+ if (!Number.isInteger(mask) || mask < 1 || mask > 0xff) {
317
+ throw new RangeError(`${this.name}: mask ${mask} must be an integer from 1 to 0xff`);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * After a write of the byte range [start, end): registers the range, then
323
+ * detects the change at once and emits `write`; inside a transaction only
324
+ * remembers the write (doc/spec-data-api.md §4.2).
325
+ *
326
+ * @param {number} start
327
+ * @param {number} end - Exclusive.
328
+ * @param {Layout_Entry | null} [entry] - The tag written by name, null for a raw write.
329
+ * @param {number} [mask] - The bits written by write_bit.
330
+ */
331
+ #written(start, end, entry = null, mask = 0xff) {
332
+ this.#observer.touch(start, end);
333
+ const errors = [];
334
+ if (this.#has_access_listeners()) {
335
+ const tags = entry ? [this.get(entry.name)] : this.#tags_in(start, end, mask);
336
+ this.#emit_access("write", tags, errors);
337
+ }
338
+ if (this.#transaction) {
339
+ this.#transaction.written = true;
340
+ return;
341
+ }
342
+ this.#observer.diff(errors);
343
+ emit_all(this, "write", [this], errors);
344
+ if (errors.length > 0) throw errors[0];
345
+ }
346
+
347
+ /** Whether an access would reach a listener of the instance or of the class. */
348
+ #has_access_listeners() {
349
+ return class_access_listeners.size > 0 || this.listenerCount("access") > 0;
350
+ }
351
+
352
+ /**
353
+ * Emits `access` (doc/spec-data-api.md §2.8) and throws the first
354
+ * exception of the listeners, for a read.
355
+ *
356
+ * @param {"read" | "write"} kind
357
+ * @param {Ltag[]} tags
358
+ */
359
+ #accessed(kind, tags) {
360
+ const errors = [];
361
+ this.#emit_access(kind, tags, errors);
362
+ if (errors.length > 0) throw errors[0];
363
+ }
364
+
365
+ /**
366
+ * Calls the `access` listeners, pushing what they throw to `errors`; inside
367
+ * a transaction only gathers the tags (doc/spec-data-api.md §4.2).
368
+ *
369
+ * @param {"read" | "write"} kind
370
+ * @param {Ltag[]} tags
371
+ * @param {unknown[]} errors
372
+ */
373
+ #emit_access(kind, tags, errors) {
374
+ if (in_access || tags.length === 0) return;
375
+ if (this.#transaction) {
376
+ const gathered = this.#transaction[kind];
377
+ for (const tag of tags) gathered.add(tag);
378
+ return;
379
+ }
380
+ in_access = true;
381
+ try {
382
+ emit_all(this, "access", [kind, tags], errors);
383
+ for (const listener of [...class_access_listeners]) {
384
+ try {
385
+ listener(this, kind, tags);
386
+ } catch (error) {
387
+ errors.push(error);
388
+ }
389
+ }
390
+ } finally {
391
+ in_access = false;
392
+ }
393
+ }
394
+
395
+ /**
396
+ * The tags that have a byte in the range [start, end), in layout order and
397
+ * without repeats; the Bools whose bit is not in `mask` are left out.
398
+ *
399
+ * @param {number} start
400
+ * @param {number} end - Exclusive.
401
+ * @param {number} [mask] - The bits of the bytes that are accessed.
402
+ * @returns {Ltag[]}
403
+ */
404
+ #tags_in(start, end, mask = 0xff) {
405
+ const entries = new Set();
406
+ const tags_at_byte = this.#compiled.tags_at_byte;
407
+ for (let i = start; i < end; i++) {
408
+ for (const entry of tags_at_byte[i]) {
409
+ if (entry.type === "bool" && (entry.mask & mask) === 0) continue;
410
+ entries.add(entry);
411
+ }
412
+ }
413
+ return [...entries].sort((a, b) => a.index - b.index).map((entry) => this.get(entry.name));
414
+ }
415
+
416
+ /**
417
+ * The gathered tags of a transaction, in layout order.
418
+ *
419
+ * @param {Set<Ltag>} tags
420
+ * @returns {Ltag[]}
421
+ */
422
+ #in_layout_order(tags) {
423
+ const entries = this.#compiled.entries;
424
+ return [...tags].sort((a, b) => entries.get(a.name).index - entries.get(b.name).index);
425
+ }
426
+
427
+ /**
428
+ * The class of a layout, built on the first use of its name: the name tells
429
+ * the layouts apart, as the registry holds one layout per name.
430
+ *
431
+ * @param {Layout} layout - A registered layout.
432
+ * @returns {typeof Ldata}
433
+ */
434
+ static #class_of(layout) {
435
+ let Layout_Data = classes.get(layout.name);
436
+ if (!Layout_Data) {
437
+ Layout_Data = Ldata.#build_class(layout);
438
+ classes.set(layout.name, Layout_Data);
439
+ }
440
+ return Layout_Data;
441
+ }
442
+
443
+ /**
444
+ * Builds the class of a layout: a subclass of Ldata whose prototype holds
445
+ * the accessor of every tag, shared by all instances of the layout. The
446
+ * accessors are defined here so that they reach the private buffer.
447
+ *
448
+ * @param {Layout} layout
449
+ * @returns {typeof Ldata}
450
+ */
451
+ static #build_class(layout) {
452
+ const Layout_Data = class extends Ldata { };
453
+ Object.defineProperty(Layout_Data, "name", { value: `Ldata_${layout.name}` });
454
+ const compiled = compiled_of(layout);
455
+ for (const entry of compiled.entries.values()) {
456
+ const { name, codec, byte, mask, size } = entry;
457
+ if (is_reserved_name(name)) {
458
+ throw new Error(`${layout.name}.${name}: reserved tag name`);
459
+ }
460
+ Object.defineProperty(Layout_Data.prototype, name, {
461
+ get() {
462
+ const value = codec.get(this.#buffer, byte, mask);
463
+ if (this.#has_access_listeners()) this.#accessed("read", [this.get(name)]);
464
+ return value;
465
+ },
466
+ set(value) {
467
+ const reason = codec.check(value);
468
+ if (reason !== null) {
469
+ const Refused_Error = typeof value === "number" ? RangeError : TypeError;
470
+ throw new Refused_Error(`${this.name}.${name}: ${reason}`);
471
+ }
472
+ codec.set(this.#buffer, byte, mask, value);
473
+ this.#written(byte, byte + size, entry);
474
+ },
475
+ enumerable: true,
476
+ configurable: false,
477
+ });
478
+ }
479
+ Layout_Data.layout = layout;
480
+ Layout_Data.compiled = compiled;
481
+ return Layout_Data;
482
+ }
483
+
484
+ /**
485
+ * Subscribes a function called for the accesses to every data block, with
486
+ * (data, kind, tags); the same function twice is one subscription
487
+ * (doc/spec-data-api.md §2.8).
488
+ *
489
+ * @param {Class_Access_Listener} listener
490
+ * @throws {TypeError} When listener is not a function.
491
+ */
492
+ static on_access(listener) {
493
+ if (typeof listener !== "function") throw new TypeError("an access listener must be a function");
494
+ class_access_listeners.add(listener);
495
+ }
496
+
497
+ /**
498
+ * Cancels a subscription of on_access; nothing happens for a function that
499
+ * is not subscribed.
500
+ *
501
+ * @param {Class_Access_Listener} listener
502
+ */
503
+ static off_access(listener) {
504
+ class_access_listeners.delete(listener);
505
+ }
506
+
507
+ /**
508
+ * Creates a data block of a layout (doc/spec-data-api.md §2.1).
509
+ *
510
+ * `new Ldata(layout, name)` returns an instance of the class built for the
511
+ * layout name (a subclass of Ldata whose prototype holds the tag
512
+ * accessors), building the class on the first use of the name.
513
+ *
514
+ * @param {Layout | string} layout - A registered layout, or its name (get_layout).
515
+ * @param {string} name - The name of the Ldata instance, a string that cannot be changed afterwards.
516
+ * @throws {TypeError} When name is not a string.
517
+ * @throws {Error} For an unknown layout name, or a layout that is not the registered one.
518
+ */
519
+ constructor(layout, name) {
520
+ super();
521
+ if (typeof name !== "string") throw new TypeError("the name of an ldata must be a string");
522
+ if (new.target === Ldata) {
523
+ const registered = resolve_layout(layout);
524
+ return new (Ldata.#class_of(registered))(registered, name);
525
+ }
526
+ this.#name = name;
527
+ this.#layout = new.target.layout;
528
+ this.#compiled = new.target.compiled;
529
+ this.setMaxListeners(100);
530
+ this.#buffer = Buffer.alloc(this.#layout.size, 0);
531
+ // init values in layout order: the bits of a combined word after the word
532
+ for (const { codec, byte, mask, init_value } of this.#compiled.entries.values()) {
533
+ codec.set(this.#buffer, byte, mask, init_value);
534
+ }
535
+ this.#observer = new Observer(this.#buffer, this.#layout.name, this.#compiled, (entry, old_value, new_value, errors) => {
536
+ // the data block event first, as when it relayed the tag events
537
+ emit_all(this, "change", [entry.name, old_value, new_value], errors);
538
+ emit_all(this.#handles.get(entry.name), "change", [old_value, new_value], errors);
539
+ });
540
+ }
541
+ }
package/src/Ltag.js ADDED
@@ -0,0 +1,87 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ /** @import { Endian, Tag_Listener, Tag_Type } from "./typedefs.js" */
4
+ /** @import { Layout_Entry } from "./internal_types.js" */
5
+
6
+ /**
7
+ * The interface of one tag of an Ldata (doc/spec-data-api.md §3). It holds no
8
+ * value: reading `value` decodes it from the buffer of the Ldata, writing it
9
+ * is the same as `ldata[name] = value`, and `load` / `store` are `load_tag` /
10
+ * `store_tag` of the Ldata for the name of the tag. `change` events
11
+ * (old_value, new_value) are emitted by the Observer of the Ldata.
12
+ */
13
+ export class Ltag extends EventEmitter {
14
+ /** @type {import("./Ldata.js").Ldata} */
15
+ #ldata;
16
+ /** @type {Layout_Entry} */
17
+ #entry;
18
+
19
+ /**
20
+ * @param {import("./Ldata.js").Ldata} ldata - The Ldata the tag belongs to.
21
+ * @param {Layout_Entry} entry - The compiled entry of the tag.
22
+ */
23
+ constructor(ldata, entry) {
24
+ super();
25
+ this.#ldata = ldata;
26
+ this.#entry = entry;
27
+ this.setMaxListeners(20);
28
+ }
29
+
30
+ /**
31
+ * Subscribes a listener; `change` is typed (old_value, new_value), any
32
+ * listener for another event name (doc/spec-data-api.md §3).
33
+ *
34
+ * @template {string | symbol} E
35
+ * @param {E} event
36
+ * @param {Tag_Listener<E>} listener
37
+ * @returns {this}
38
+ */
39
+ on(event, listener) { return super.on(event, listener); }
40
+
41
+ /**
42
+ * Subscribes a listener for one emission, typed like `on`.
43
+ *
44
+ * @template {string | symbol} E
45
+ * @param {E} event
46
+ * @param {Tag_Listener<E>} listener
47
+ * @returns {this}
48
+ */
49
+ once(event, listener) { return super.once(event, listener); }
50
+
51
+ /**
52
+ * Removes a listener, typed like `on`.
53
+ *
54
+ * @template {string | symbol} E
55
+ * @param {E} event
56
+ * @param {Tag_Listener<E>} listener
57
+ * @returns {this}
58
+ */
59
+ off(event, listener) { return super.off(event, listener); }
60
+
61
+ /** @returns {string} */
62
+ get name() { return this.#entry.name; }
63
+ /** @returns {Tag_Type} Lower case type name. */
64
+ get type() { return this.#entry.type; }
65
+ /** @returns {number} Size in bytes; a Bool has the byte it lies in. */
66
+ get size() { return this.#entry.size; }
67
+ /** @returns {number | boolean} */
68
+ get value() { return this.#ldata[this.#entry.name]; }
69
+ /** @param {number | boolean} value */
70
+ set value(value) { this.#ldata[this.#entry.name] = value; }
71
+
72
+ /**
73
+ * Same as `ldata.load_tag(name, io_buffer, endian)` (doc/spec-data-api.md §2.9).
74
+ *
75
+ * @param {Uint8Array} io_buffer
76
+ * @param {Endian} endian
77
+ */
78
+ load(io_buffer, endian) { this.#ldata.load_tag(this.#entry.name, io_buffer, endian); }
79
+
80
+ /**
81
+ * Same as `ldata.store_tag(name, io_buffer, endian)` (doc/spec-data-api.md §2.9).
82
+ *
83
+ * @param {Uint8Array} io_buffer
84
+ * @param {Endian} endian
85
+ */
86
+ store(io_buffer, endian) { this.#ldata.store_tag(this.#entry.name, io_buffer, endian); }
87
+ }
package/src/codecs.js ADDED
@@ -0,0 +1,64 @@
1
+ // Codecs of the tag types on a big-endian buffer (doc/design.md §4).
2
+ // `byte` is the byte offset of the tag; `mask` is the bit mask of a Bool.
3
+ // Device byte order is handled at the IO boundary, not here.
4
+
5
+ /** The largest finite float32 value; a finite number above it is refused by a Real. */
6
+ export const FLOAT32_MAX = 3.4028234663852886e38;
7
+
8
+ const read_bool = (buffer, byte, mask) => (buffer[byte] & mask) !== 0;
9
+ const write_bool = (buffer, byte, mask, value) => {
10
+ buffer[byte] = value ? buffer[byte] | mask : buffer[byte] & ~mask;
11
+ };
12
+
13
+ /** A Bool takes any value, by truthiness. */
14
+ const check_bool = () => null;
15
+
16
+ /**
17
+ * The check of an integer type: a number that is an integer inside the range.
18
+ *
19
+ * @param {number} min
20
+ * @param {number} max
21
+ * @returns {(value: any) => string | null}
22
+ */
23
+ const check_integer = (min, max) => (value) => {
24
+ if (typeof value !== "number") return `a ${typeof value} is not a number`;
25
+ if (!Number.isInteger(value)) return `${value} is not an integer`;
26
+ if (value < min || value > max) return `${value} is outside ${min}..${max}`;
27
+ return null;
28
+ };
29
+
30
+ /** The check of a Real: any number, NaN and the infinities included, but no finite value beyond float32. */
31
+ const check_real = (value) => {
32
+ if (typeof value !== "number") return `a ${typeof value} is not a number`;
33
+ if (Number.isFinite(value) && Math.abs(value) > FLOAT32_MAX) return `${value} is beyond the float32 range`;
34
+ return null;
35
+ };
36
+
37
+ const check_byte = check_integer(0, 0xff);
38
+ const check_sint = check_integer(-0x80, 0x7f);
39
+ const check_int = check_integer(-0x8000, 0x7fff);
40
+ const check_uint = check_integer(0, 0xffff);
41
+ const check_dint = check_integer(-0x80000000, 0x7fffffff);
42
+ const check_udint = check_integer(0, 0xffffffff);
43
+
44
+ /**
45
+ * Codec of each tag type (lower case): its size in bytes, `check(value)`, which
46
+ * returns null for a value that can be written and the reason otherwise,
47
+ * `get(buffer, byte, mask)` and `set(buffer, byte, mask, value)`. `set` is
48
+ * called with a checked value only (doc/spec-data-api.md §2.3).
49
+ *
50
+ * @type {Record<string, import("./internal_types.js").Codec>}
51
+ */
52
+ export const CODECS = {
53
+ bool: { size: 1, check: check_bool, get: read_bool, set: write_bool },
54
+ byte: { size: 1, check: check_byte, get: (b, i) => b.readUInt8(i), set: (b, i, _m, v) => b.writeUInt8(v, i) },
55
+ usint: { size: 1, check: check_byte, get: (b, i) => b.readUInt8(i), set: (b, i, _m, v) => b.writeUInt8(v, i) },
56
+ sint: { size: 1, check: check_sint, get: (b, i) => b.readInt8(i), set: (b, i, _m, v) => b.writeInt8(v, i) },
57
+ int: { size: 2, check: check_int, get: (b, i) => b.readInt16BE(i), set: (b, i, _m, v) => b.writeInt16BE(v, i) },
58
+ uint: { size: 2, check: check_uint, get: (b, i) => b.readUInt16BE(i), set: (b, i, _m, v) => b.writeUInt16BE(v, i) },
59
+ word: { size: 2, check: check_uint, get: (b, i) => b.readUInt16BE(i), set: (b, i, _m, v) => b.writeUInt16BE(v, i) },
60
+ dint: { size: 4, check: check_dint, get: (b, i) => b.readInt32BE(i), set: (b, i, _m, v) => b.writeInt32BE(v, i) },
61
+ udint: { size: 4, check: check_udint, get: (b, i) => b.readUInt32BE(i), set: (b, i, _m, v) => b.writeUInt32BE(v, i) },
62
+ dword: { size: 4, check: check_udint, get: (b, i) => b.readUInt32BE(i), set: (b, i, _m, v) => b.writeUInt32BE(v, i) },
63
+ real: { size: 4, check: check_real, get: (b, i) => b.readFloatBE(i), set: (b, i, _m, v) => b.writeFloatBE(v, i) },
64
+ };