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.
@@ -0,0 +1,147 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ /** @import { Compiled_Layout, Layout_Entry } from "./internal_types.js" */
4
+
5
+ // a change made by a change handler is detected in a further round
6
+ const MAX_DIFF_ROUNDS = 16;
7
+
8
+ const same_value = (a, b) => a === b || (Number.isNaN(a) && Number.isNaN(b));
9
+
10
+ /**
11
+ * Detects the changes of an Ldata buffer by comparing it with a snapshot
12
+ * (doc/design.md §5.1) and reports each changed tag once.
13
+ *
14
+ * Every write path of the Ldata registers the byte range it wrote with
15
+ * touch(), then calls diff() (in a transaction: at its commit). Only the
16
+ * registered range is compared, the other bytes equal the snapshot by
17
+ * construction. A diff() called while the changes are being reported, by a
18
+ * handler writing a tag, is only recorded; the outer diff() runs another
19
+ * round once the current changes are reported.
20
+ */
21
+ export class Observer {
22
+ /** @type {Buffer} */
23
+ #buffer;
24
+ /** @type {string} */
25
+ #name;
26
+ /** @type {Compiled_Layout} */
27
+ #compiled;
28
+ /** @type {Buffer} */
29
+ #snapshot;
30
+ /** @type {(entry: Layout_Entry, old_value: any, new_value: any, errors: unknown[]) => void} */
31
+ #report;
32
+ #diffing = false;
33
+ #pending = false;
34
+ /** The byte range [#start, #end) written since the last collection. */
35
+ #start = Number.POSITIVE_INFINITY;
36
+ #end = 0;
37
+ /** @type {Uint32Array} */
38
+ #seen;
39
+ #mark = 0;
40
+
41
+ /**
42
+ * @param {Buffer} buffer - The buffer of the Ldata.
43
+ * @param {string} name - The layout name, for errors.
44
+ * @param {Compiled_Layout} compiled - The compiled data of the layout of the Ldata.
45
+ * @param {(entry: Layout_Entry, old_value: any, new_value: any, errors: unknown[]) => void} report - Called for each change; what a handler throws goes to `errors`.
46
+ */
47
+ constructor(buffer, name, compiled, report) {
48
+ this.#buffer = buffer;
49
+ this.#name = name;
50
+ this.#compiled = compiled;
51
+ this.#report = report;
52
+ this.#snapshot = Buffer.from(buffer);
53
+ this.#seen = new Uint32Array(compiled.entries.size);
54
+ }
55
+
56
+ /**
57
+ * Registers the byte range [start, end) as written.
58
+ *
59
+ * @param {number} start
60
+ * @param {number} end - Exclusive.
61
+ */
62
+ touch(start, end) {
63
+ if (start >= end) return;
64
+ if (start < this.#start) this.#start = start;
65
+ if (end > this.#end) this.#end = end;
66
+ }
67
+
68
+ /**
69
+ * Forgets the registered range: the buffer was restored to the snapshot.
70
+ */
71
+ discard() {
72
+ this.#start = Number.POSITIVE_INFINITY;
73
+ this.#end = 0;
74
+ }
75
+
76
+ /**
77
+ * Reports every tag changed since the last diff. Nothing is thrown: what
78
+ * the handlers throw, and the exception of changes that do not settle, are
79
+ * pushed to `errors` after every change has been reported (doc/spec-data-api.md §4.4).
80
+ *
81
+ * @param {unknown[]} errors
82
+ */
83
+ diff(errors) {
84
+ if (this.#diffing) {
85
+ this.#pending = true;
86
+ return;
87
+ }
88
+ this.#diffing = true;
89
+ try {
90
+ let rounds = 0;
91
+ do {
92
+ this.#pending = false;
93
+ if (++rounds > MAX_DIFF_ROUNDS) {
94
+ errors.push(new Error(`${this.#name}: changes do not settle after ${MAX_DIFF_ROUNDS} rounds`));
95
+ break;
96
+ }
97
+ for (const [entry, old_value, new_value] of this.#collect()) {
98
+ this.#report(entry, old_value, new_value, errors);
99
+ }
100
+ } while (this.#pending);
101
+ } finally {
102
+ this.#diffing = false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Collects the changed tags, in layout order, and updates the snapshot.
108
+ * Old values are decoded from the snapshot, so no value is cached.
109
+ */
110
+ #collect() {
111
+ const buffer = this.#buffer;
112
+ const snapshot = this.#snapshot;
113
+ const tags_at_byte = this.#compiled.tags_at_byte;
114
+ // entries already checked in this collection, by entry index
115
+ const seen = this.#seen;
116
+ if (this.#mark === 0xffffffff) {
117
+ // the marks wrap around: clear them
118
+ seen.fill(0);
119
+ this.#mark = 0;
120
+ }
121
+ const mark = ++this.#mark;
122
+ const changes = [];
123
+ let in_order = true;
124
+ let last_index = -1;
125
+ // a write during the reporting registers its range for the next round
126
+ const start = this.#start;
127
+ const end = Math.min(this.#end, buffer.length);
128
+ this.discard();
129
+ for (let i = start; i < end; i++) {
130
+ if (buffer[i] === snapshot[i]) continue;
131
+ for (const entry of tags_at_byte[i]) {
132
+ if (seen[entry.index] === mark) continue;
133
+ seen[entry.index] = mark;
134
+ const { codec, byte, mask } = entry;
135
+ const old_value = codec.get(snapshot, byte, mask);
136
+ const new_value = codec.get(buffer, byte, mask);
137
+ if (same_value(old_value, new_value)) continue;
138
+ changes.push([entry, old_value, new_value]);
139
+ if (entry.index < last_index) in_order = false;
140
+ last_index = entry.index;
141
+ }
142
+ }
143
+ if (start < end) buffer.copy(snapshot, start, start, end);
144
+ if (!in_order) changes.sort((a, b) => a[0].index - b[0].index);
145
+ return changes;
146
+ }
147
+ }
@@ -0,0 +1,83 @@
1
+ // The registry of the compiled layouts, by layout name (doc/design.md §3).
2
+ // A name identifies one layout: it is the only source of the layouts an Ldata
3
+ // is created from. Written by register_structure (layout.js), read by Ldata.
4
+
5
+ /** @import { Layout } from "./typedefs.js" */
6
+ /** @import { Compiled_Layout } from "./internal_types.js" */
7
+
8
+ /**
9
+ * @typedef {object} Registered
10
+ * @property {string} key - The canonical text of the structure, to tell the same layout from another one.
11
+ * @property {Layout} layout
12
+ */
13
+
14
+ /** @type {Map<string, Registered>} */
15
+ const registered = new Map();
16
+
17
+ /**
18
+ * The compiled data of the layouts (doc/design.md §3), kept here so that the
19
+ * public Layout holds none of it.
20
+ *
21
+ * @type {WeakMap<Layout, Compiled_Layout>}
22
+ */
23
+ const compiled = new WeakMap();
24
+
25
+ /**
26
+ * @param {Layout} layout
27
+ * @param {Compiled_Layout} data
28
+ */
29
+ export function set_compiled(layout, data) {
30
+ compiled.set(layout, data);
31
+ }
32
+
33
+ /**
34
+ * The compiled data of a layout made by this package.
35
+ *
36
+ * @param {Layout} layout
37
+ * @returns {Compiled_Layout}
38
+ * @throws {Error} For an object that is not a compiled layout.
39
+ */
40
+ export function compiled_of(layout) {
41
+ const data = compiled.get(layout);
42
+ if (!data) throw new Error(`${layout?.name} is not a registered layout`);
43
+ return data;
44
+ }
45
+
46
+ /**
47
+ * @param {string} name
48
+ * @returns {Registered | undefined}
49
+ */
50
+ export function registered_of(name) {
51
+ return registered.get(name);
52
+ }
53
+
54
+ /**
55
+ * @param {string} name
56
+ * @param {Registered} entry
57
+ */
58
+ export function register(name, entry) {
59
+ registered.set(name, entry);
60
+ }
61
+
62
+ /**
63
+ * The registered layout of a name, undefined when there is none.
64
+ *
65
+ * @param {string} name
66
+ * @returns {Layout | undefined}
67
+ */
68
+ export function find_layout(name) {
69
+ return registered.get(name)?.layout;
70
+ }
71
+
72
+ /**
73
+ * The registered layout of a name (doc/spec-data-api.md §6.5).
74
+ *
75
+ * @param {string} name
76
+ * @returns {Layout}
77
+ * @throws {Error} When no layout of that name is registered.
78
+ */
79
+ export function get_layout(name) {
80
+ const layout = find_layout(name);
81
+ if (!layout) throw new Error(`unknown layout ${name}`);
82
+ return layout;
83
+ }
@@ -0,0 +1,35 @@
1
+ // The reserved names (doc/spec-data-api.md §2.10, doc/design.md §4). Internal:
2
+ // index.js does not export this file.
3
+
4
+ /**
5
+ * Names a tag must not have: the members of Ldata, the method and field names
6
+ * of EventEmitter in Node.js 20.11 and the names of Object.prototype. A static
7
+ * list, so that it does not depend on the Node.js version that runs the program.
8
+ */
9
+ const RESERVED_NAMES = new Set([
10
+ // members of Ldata
11
+ "name", "layout", "size", "get", "transaction",
12
+ "read_bytes", "write_bytes", "read_bit", "write_bit", "load_tag", "store_tag",
13
+ // EventEmitter
14
+ "addListener", "emit", "eventNames", "getMaxListeners", "listenerCount", "listeners",
15
+ "off", "on", "once", "prependListener", "prependOnceListener", "rawListeners",
16
+ "removeAllListeners", "removeListener", "setMaxListeners",
17
+ "_events", "_eventsCount", "_maxListeners",
18
+ // Object
19
+ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
20
+ "toLocaleString", "toString", "valueOf", "__proto__",
21
+ "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__",
22
+ ]);
23
+
24
+ /** Event names of the library; a user does not emit them or use them for its own events. */
25
+ export const RESERVED_EVENTS = Object.freeze(["change", "write", "access", "read"]);
26
+
27
+ /**
28
+ * Whether a tag name would shadow a member of a data block.
29
+ *
30
+ * @param {string} name
31
+ * @returns {boolean}
32
+ */
33
+ export function is_reserved_name(name) {
34
+ return RESERVED_NAMES.has(name);
35
+ }
@@ -0,0 +1,238 @@
1
+ // Expansion and validation of data block structures (doc/spec.md §3,
2
+ // doc/design.md §3.1). Part of the data layer.
3
+ import { FLOAT32_MAX } from "./codecs.js";
4
+ import { is_reserved_name } from "./reserved.js";
5
+
6
+ /** @import { Layout_Item } from "./typedefs.js" */
7
+ /** @import { Data_Struct } from "./internal_types.js" */
8
+
9
+ function add_item(data_struct, item, base_offset, name_prefix) {
10
+ const items = data_struct.items;
11
+ // every entry that is not a pure inclusion is a tag, whatever its name: duplicated
12
+ // and invalid names are kept and reported by validate_structure
13
+ if (item.includes === undefined || item.name !== undefined) items.push(item);
14
+ if (name_prefix && typeof item.name === "string") item.name = name_prefix + item.name;
15
+
16
+ item.offset = item.offset + base_offset;
17
+ const length = item.offset + item.length;
18
+ if (data_struct.length < length) data_struct.length = length;
19
+
20
+ const includes = item.includes;
21
+ if (includes && Array.isArray(includes)) {
22
+ for (const _item of includes) {
23
+ add_item(data_struct, { ..._item }, item.offset, item.name_prefix);
24
+ }
25
+ // The "includes" and "coupling" attributes can only be chosen one at a time.
26
+ return;
27
+ }
28
+
29
+ const coupling = [];
30
+ const _coupling = item.coupling;
31
+ if (_coupling && Array.isArray(_coupling)) {
32
+ for (const _cp of _coupling) {
33
+ const cp = { ..._cp };
34
+ add_item(data_struct, cp, item.offset, item.name_prefix);
35
+ cp.coupling ??= [];
36
+ cp.coupling.push(item.name);
37
+ coupling.push(cp);
38
+ }
39
+ share_init_value(item, coupling);
40
+ }
41
+ item.coupling = coupling.map((item) => item.name);
42
+ }
43
+
44
+ // Items whose init_value is given both on the word and on some of its bits,
45
+ // with the names of those bits; reported by validate_structure.
46
+ const init_conflicts = new WeakMap();
47
+
48
+ /**
49
+ * Only one side of a combined word gives the init values (doc/spec.md §3):
50
+ * either the word, whose bits then take theirs from it, or the bits, from
51
+ * which the word is composed (a bit without init_value is false).
52
+ */
53
+ function share_init_value(word, bits) {
54
+ const given = bits.filter((bit) => bit.init_value !== undefined);
55
+ if (word.init_value !== undefined && given.length > 0) {
56
+ // keep the given values (the bits are written after the word) and
57
+ // report the conflict; the other bits still take theirs from the word
58
+ init_conflicts.set(word, given.map((bit) => bit.name));
59
+ }
60
+ const size = word.length >> 3;
61
+ // big-endian word: byte k of the word holds bits (size - 1 - k) * 8 ..
62
+ // a bit outside the word has no value in it (reported by validate_structure)
63
+ const bit_value = (bit) => {
64
+ const relative = bit.offset - word.offset;
65
+ if (!(relative >= 0 && relative < word.length)) return 0;
66
+ return 2 ** ((size - 1 - (relative >> 3)) * 8 + relative % 8);
67
+ };
68
+ if (word.init_value === undefined) {
69
+ word.init_value = 0;
70
+ for (const bit of bits) {
71
+ bit.init_value ??= false;
72
+ if (bit.init_value === true) word.init_value += bit_value(bit);
73
+ }
74
+ return;
75
+ }
76
+ if (typeof word.init_value !== "number") return;
77
+ for (const bit of bits) {
78
+ if (bit.init_value !== undefined) continue;
79
+ const value = bit_value(bit);
80
+ bit.init_value = value > 0 && Math.floor(word.init_value / value) % 2 === 1;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Expands a list of items (resolving `includes` and `coupling`, making offsets
86
+ * absolute) into the given structure. Internal. It changes the items it is given.
87
+ *
88
+ * @param {{name: string}} data_struct - The structure to fill in; only the name is given.
89
+ * @param {Layout_Item[]} items
90
+ * @returns {Data_Struct} The same object, now complete.
91
+ */
92
+ export function build_structure(data_struct, items) {
93
+ data_struct.length = 0;
94
+ data_struct.items = [];
95
+ if (!items) return data_struct;
96
+ for (const item of items) {
97
+ add_item(data_struct, item, 0);
98
+ }
99
+ return data_struct;
100
+ }
101
+
102
+ /**
103
+ * Bit length of each tag type (lower case names).
104
+ *
105
+ * @type {Record<string, number>}
106
+ */
107
+ export const TYPE_BITS = {
108
+ bool: 1,
109
+ byte: 8, sint: 8, usint: 8,
110
+ int: 16, uint: 16, word: 16,
111
+ dint: 32, udint: 32, dword: 32, real: 32,
112
+ };
113
+
114
+ /**
115
+ * A tag name: letters, digits and `_`, not starting with a digit, an
116
+ * identifier in every language (doc/spec-data-api.md §6.2).
117
+ */
118
+ const TAG_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
119
+
120
+ // value range of each integer type
121
+ const INTEGER_RANGES = {
122
+ byte: [0, 0xff], usint: [0, 0xff], sint: [-0x80, 0x7f],
123
+ int: [-0x8000, 0x7fff], uint: [0, 0xffff], word: [0, 0xffff],
124
+ dint: [-0x80000000, 0x7fffffff], udint: [0, 0xffffffff], dword: [0, 0xffffffff],
125
+ };
126
+
127
+ function check_init_value(item, type) {
128
+ const value = item.init_value;
129
+ if (type === "bool") return typeof value === "boolean";
130
+ if (typeof value !== "number") return false;
131
+ if (type === "real") return Number.isFinite(value) && Math.abs(value) <= FLOAT32_MAX;
132
+ const [min, max] = INTEGER_RANGES[type];
133
+ return Number.isInteger(value) && value >= min && value <= max;
134
+ }
135
+
136
+ /**
137
+ * Validates a structure built by build_structure against the layout rules
138
+ * (doc/spec.md §3, doc/design.md §3.1). Internal.
139
+ *
140
+ * @param {Data_Struct} data_struct
141
+ * @returns {string[]} descriptions of all violations, empty when the layout is valid
142
+ */
143
+ export function validate_structure(data_struct) {
144
+ const errors = [];
145
+ const struct_name = data_struct.name;
146
+ const report = (item, message) => errors.push(`${struct_name}.${item.name}: ${message}`);
147
+ const items = data_struct.items;
148
+
149
+ // names
150
+ const by_name = new Map();
151
+ for (const item of items) {
152
+ if (by_name.has(item.name)) report(item, "duplicated name");
153
+ else by_name.set(item.name, item);
154
+ if (typeof item.name !== "string" || !TAG_NAME.test(item.name)) {
155
+ report(item, "name is not an identifier (letters, digits and \"_\", not starting with a digit)");
156
+ } else if (is_reserved_name(item.name)) {
157
+ report(item, "name is a reserved name");
158
+ }
159
+ }
160
+
161
+ // type, length, alignment and init_value
162
+ const valid_items = [];
163
+ for (const item of items) {
164
+ const type = item.type?.toLowerCase();
165
+ if (!(type in TYPE_BITS)) {
166
+ report(item, `unknown type ${item.type}`);
167
+ continue;
168
+ }
169
+ if (item.length !== TYPE_BITS[type]) {
170
+ report(item, `length ${item.length} does not match type ${item.type}`);
171
+ }
172
+ if (type !== "bool") {
173
+ if (item.offset % 8 !== 0) {
174
+ report(item, `offset ${item.offset} is not on a byte boundary`);
175
+ } else if (TYPE_BITS[type] > 8 && (item.offset >> 3) % 2 !== 0) {
176
+ report(item, `byte offset ${item.offset >> 3} is not even`);
177
+ }
178
+ }
179
+ const conflict = init_conflicts.get(item);
180
+ if (conflict) {
181
+ report(item, `init_value is given both on the word and on its bits ${conflict.join(", ")}`);
182
+ }
183
+ if (!check_init_value(item, type)) {
184
+ report(item, `init_value ${item.init_value} is not valid for type ${item.type}`);
185
+ }
186
+ valid_items.push({ item, type, end: item.offset + TYPE_BITS[type] });
187
+ }
188
+
189
+ // non-Bool tags must not overlap
190
+ const words = valid_items
191
+ .filter(({ type }) => type !== "bool")
192
+ .sort((a, b) => a.item.offset - b.item.offset);
193
+ let last = null;
194
+ for (const word of words) {
195
+ if (last && word.item.offset < last.end) {
196
+ report(word.item, `overlaps ${last.item.name}`);
197
+ }
198
+ if (!last || word.end > last.end) last = word;
199
+ }
200
+
201
+ // each bit belongs to at most one Bool, and a Bool inside a non-Bool tag
202
+ // must be one of its coupled bits
203
+ const bits = new Map();
204
+ for (const { item, type } of valid_items) {
205
+ if (type !== "bool") continue;
206
+ const other = bits.get(item.offset);
207
+ if (other) report(item, `shares bit ${item.offset} with ${other.name}`);
208
+ else bits.set(item.offset, item);
209
+ for (const word of words) {
210
+ if (item.offset < word.item.offset || item.offset >= word.end) continue;
211
+ if (!item.coupling?.includes(word.item.name)) {
212
+ report(item, `lies inside ${word.item.name} but is not one of its coupled bits`);
213
+ }
214
+ }
215
+ }
216
+
217
+ // coupled bits of a combined tag must lie inside it
218
+ for (const { item, type, end } of valid_items) {
219
+ if (type === "bool" || !item.coupling?.length) continue;
220
+ for (const bit_name of item.coupling) {
221
+ const bit = by_name.get(bit_name);
222
+ if (!bit) {
223
+ report(item, `coupled bit ${bit_name} does not exist`);
224
+ } else if (bit.type?.toLowerCase() !== "bool") {
225
+ report(item, `coupled tag ${bit_name} is not a Bool`);
226
+ } else if (bit.offset < item.offset || bit.offset >= end) {
227
+ report(item, `coupled bit ${bit_name} lies outside it`);
228
+ }
229
+ }
230
+ }
231
+
232
+ // whole registers
233
+ if (data_struct.length % 16 !== 0) {
234
+ errors.push(`${struct_name}: length ${data_struct.length} bits is not a whole number of registers`);
235
+ }
236
+
237
+ return errors;
238
+ }
@@ -0,0 +1,143 @@
1
+ // Type definitions of the public interface. This file has no run-time content:
2
+ // the JSDoc here is the source of the generated declaration file (dist/types).
3
+ // The types of the internals are in internal_types.js and are not exported.
4
+
5
+ /**
6
+ * An entry of a layout description that defines a tag (doc/spec-data-api.md §6.2).
7
+ *
8
+ * @typedef {object} Tag_Item
9
+ * @property {string} name - Tag name.
10
+ * @property {string} type - Tag type, case-insensitive (see Tag_Type).
11
+ * @property {number} offset - Bit offset, relative to the parent entry until expanded.
12
+ * @property {number} [length] - Length in bits, filled in from the type when omitted.
13
+ * @property {number | boolean} [init_value] - Initial value.
14
+ * @property {string} [comment] - Description, rendered into the generated types.
15
+ * @property {boolean} [is_combined] - Descriptive mark of a combined word; the linkage is decided by `coupling`.
16
+ * @property {Tag_Item[] | string} [coupling] - The bits of a combined word, as an inline list or the name of a fragment.
17
+ * @property {never} [includes] - A tag entry does not include entries.
18
+ */
19
+
20
+ /**
21
+ * An entry of a layout description that splices in other entries (doc/spec-data-api.md §6.2).
22
+ *
23
+ * @typedef {object} Include_Item
24
+ * @property {Layout_Item[] | string} includes - The entries to include, as an inline list or the name of a fragment.
25
+ * @property {number} offset - Bit offset by which the included entries are shifted as a whole.
26
+ * @property {string} [name_prefix] - Prefix of the names of the included entries.
27
+ * @property {never} [name] - An inclusion defines no tag.
28
+ * @property {never} [type] - An inclusion defines no tag.
29
+ */
30
+
31
+ /**
32
+ * One entry of a layout description, as written by the user in a layout file
33
+ * or given to register_layout (doc/spec-data-api.md §6).
34
+ *
35
+ * @typedef {Tag_Item | Include_Item} Layout_Item
36
+ */
37
+
38
+ /**
39
+ * A layout description: `layouts` maps layout names to lists of entries, and
40
+ * every other key is a fragment, a list of entries that can be named
41
+ * (doc/spec-data-api.md §6.1).
42
+ *
43
+ * @typedef {{ layouts?: Record<string, Layout_Item[]> } & Record<string, Layout_Item[] | Record<string, Layout_Item[]> | undefined>} Layout_Description
44
+ */
45
+
46
+ /**
47
+ * One tag of a compiled layout (doc/spec-data-api.md §6.6), read-only:
48
+ * `name`; `type`, as written in the description; `offset`, the absolute bit
49
+ * offset; `length` in bits; `init_value`; `comment`, if given; `coupling`,
50
+ * the tag names of the bits of a combined word, or of the combined words a
51
+ * bit belongs to.
52
+ *
53
+ * @typedef {Readonly<{
54
+ * name: string,
55
+ * type: string,
56
+ * offset: number,
57
+ * length: number,
58
+ * init_value: number | boolean,
59
+ * comment?: string,
60
+ * coupling: readonly string[],
61
+ * }>} Layout_Tag
62
+ */
63
+
64
+ /**
65
+ * A compiled layout (doc/spec-data-api.md §6.6), read-only and deeply frozen:
66
+ * `name`; `length` in bits; `size` in bytes; `items`, the tags in layout
67
+ * order (a combined word before its bits).
68
+ *
69
+ * @typedef {Readonly<{
70
+ * name: string,
71
+ * length: number,
72
+ * size: number,
73
+ * items: readonly Layout_Tag[],
74
+ * }>} Layout
75
+ */
76
+
77
+ /**
78
+ * Device byte order of an IO buffer (doc/spec-data-api.md §2.9).
79
+ *
80
+ * @typedef {"BE" | "LE" | "BEBS" | "LEBS"} Endian
81
+ */
82
+
83
+ /**
84
+ * A source of layouts: the path of a YAML 1.2 layout file, or a description
85
+ * that is already parsed (the same structure as the file, doc/spec-data-api.md §6.1).
86
+ *
87
+ * @typedef {string | Layout_Description} Layout_Source
88
+ */
89
+
90
+ /**
91
+ * The result of registering a layout description.
92
+ *
93
+ * @typedef {object} Registered_Layouts
94
+ * @property {Record<string, Layout>} layouts
95
+ * @property {Record<string, Layout_Item[]>} fragments
96
+ */
97
+
98
+ /**
99
+ * Type of a tag, in lower case: the type of `Ltag.type` (doc/spec-data-api.md §1).
100
+ *
101
+ * @typedef {"bool" | "byte" | "usint" | "sint" | "int" | "uint" | "word" | "dint" | "udint" | "dword" | "real"} Tag_Type
102
+ */
103
+
104
+ /**
105
+ * The listeners of the events of an Ldata that the library emits.
106
+ *
107
+ * @typedef {object} Data_Events
108
+ * @property {(name: string, old_value: number | boolean, new_value: number | boolean) => void} change
109
+ * @property {(data: import("./Ldata.js").Ldata) => void} write
110
+ * @property {(kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} access
111
+ */
112
+
113
+ /**
114
+ * A class-level access listener: called for the accesses to every Ldata
115
+ * (doc/spec-data-api.md §2.8).
116
+ *
117
+ * @typedef {(data: import("./Ldata.js").Ldata, kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} Class_Access_Listener
118
+ */
119
+
120
+ /**
121
+ * The listener of an event of an Ldata: typed for `change` and `write`, any
122
+ * listener for another name (doc/spec-data-api.md §2.7).
123
+ *
124
+ * @template {string | symbol} E
125
+ * @typedef {E extends keyof Data_Events ? Data_Events[E] : (...args: any[]) => void} Data_Listener
126
+ */
127
+
128
+ /**
129
+ * The listeners of the events of an Ltag that the library emits.
130
+ *
131
+ * @typedef {object} Tag_Events
132
+ * @property {(old_value: number | boolean, new_value: number | boolean) => void} change
133
+ */
134
+
135
+ /**
136
+ * The listener of an event of an Ltag: typed for `change`, any listener for
137
+ * another name (doc/spec-data-api.md §3).
138
+ *
139
+ * @template {string | symbol} E
140
+ * @typedef {E extends keyof Tag_Events ? Tag_Events[E] : (...args: any[]) => void} Tag_Listener
141
+ */
142
+
143
+ export {};