layout-data 0.2.0 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0
4
+
5
+ ### Breaking changes
6
+
7
+ - **`access` is gathered in a transaction.** Inside `transaction(fn)` no `access` is emitted at once: the tags read and the tags written are gathered apart, and at the end at most one `access` of kind `read` and one of kind `write` are emitted, each with its tags in layout order and without repeats. At commit they come before the `change` events and `write`; on rollback only the `read` one is emitted, as those reads did happen, and the undone writes report nothing. The exceptions of `access` listeners follow the rules of the other handlers at commit, and are dropped on rollback.
8
+ - **`read_bit` is a reserved name.** A layout with a tag named `read_bit` no longer registers. Before 1.0 the list of reserved names may still grow; from 1.0 on new members are added only as `Symbol`-keyed members or static methods.
9
+
10
+ ### Features
11
+
12
+ - **`read_bit(offset, mask)`** reads the bits of a byte selected by `mask` and returns `true` when any of them is set. Its `access` names only the Bools whose bit is read, as `write_bit` does for writes, instead of every tag of the byte as `read_bytes` would.
13
+
14
+ ### Packaging
15
+
16
+ - `yaml` is no longer bundled: it is a run-time dependency, and the package ships `src` as is, with the declarations in `dist/types`.
17
+
18
+ ## v0.3.0 (中文)
19
+
20
+ ### 破坏性变更
21
+
22
+ - **事务内汇总 `access`。** 在 `transaction(fn)` 内不再立即发出 `access`:被读的 tag 和被写的 tag 分别汇总,结束时最多发出一次 `read` 类和一次 `write` 类的 `access`,tag 按布局顺序且不重复。提交时它们先于 `change` 事件和 `write`;回滚时只发出 `read` 类,因为那些读确实发生了,被撤销的写不报告。`access` 监听函数的异常在提交时与其他处理器的规则相同,回滚时丢弃。
23
+ - **`read_bit` 成为保留名。** 含名为 `read_bit` 的 tag 的布局不再能注册。1.0 之前保留名清单仍可能增加;从 1.0 起新成员只以 `Symbol` 键成员或静态方法的形式加入。
24
+
25
+ ### 功能
26
+
27
+ - **`read_bit(offset, mask)`** 读取一个字节中由 `mask` 选中的位,任一位置位即返回 `true`。它的 `access` 只列出被读位的 Bool,与 `write_bit` 对写的处理一致,而不像 `read_bytes` 那样列出该字节的所有 tag。
28
+
29
+ ### 打包
30
+
31
+ - 不再内置 `yaml`:它成为运行时依赖,包直接发布 `src`,类型声明在 `dist/types`。
32
+
3
33
  ## v0.2.0
4
34
 
5
35
  The first release.
package/README.md CHANGED
@@ -22,8 +22,6 @@ Requires Node.js >= 20.11. The package is ESM only.
22
22
  pnpm add layout-data
23
23
  ```
24
24
 
25
- `yaml` is bundled into the library, so nothing else needs to be installed.
26
-
27
25
  ## Usage
28
26
 
29
27
  Describe the layout (`pump.yaml`). Entries other than `layouts` are fragments that entries can reference by name. Offsets are in bits.
@@ -114,7 +112,7 @@ The specification and design documents are in [doc/](doc/spec.md) (English, `*.m
114
112
 
115
113
  ```bash
116
114
  pnpm install
117
- pnpm build # bundle to dist/ and generate dist/types
115
+ pnpm build # generate dist/types
118
116
  pnpm test # build, type tests, node --test
119
117
  pnpm lint
120
118
  ```
@@ -91,11 +91,12 @@ export class Ldata extends EventEmitter<[never]> {
91
91
  */
92
92
  get(tagname: string): Ltag | undefined;
93
93
  /**
94
- * Runs fn synchronously and makes its writes one whole: no event is emitted
95
- * during it; when fn returns, one detection runs and one `write` is emitted
96
- * if fn wrote; when fn throws, the buffer is restored and no event is
97
- * emitted (doc/spec-data-api.md §4.2). The return value of fn is not
98
- * inspected: a Promise is neither awaited nor rejected.
94
+ * Runs fn synchronously and makes its accesses and writes one whole: no
95
+ * event is emitted during it; when fn returns, the gathered `access` is
96
+ * emitted, one detection runs and one `write` is emitted if fn wrote; when
97
+ * fn throws, the buffer is restored and only the gathered `access` of kind
98
+ * `read` is emitted (doc/spec-data-api.md §4.2). The return value of fn is
99
+ * not inspected: a Promise is neither awaited nor rejected.
99
100
  *
100
101
  * @template T
101
102
  * @param {() => T} fn
@@ -122,6 +123,16 @@ export class Ldata extends EventEmitter<[never]> {
122
123
  * @throws {TypeError} When source is not a Uint8Array.
123
124
  */
124
125
  write_bytes(offset: number, source: Uint8Array): void;
126
+ /**
127
+ * Reads the bits selected by mask of the byte at offset
128
+ * (doc/spec-data-api.md §2.5); `access` names only the Bools of those bits.
129
+ *
130
+ * @param {number} offset - Byte offset.
131
+ * @param {number} mask - An integer from 1 to 0xff.
132
+ * @returns {boolean} Whether any of the bits is set.
133
+ * @throws {RangeError} For an offset out of range, or a mask that is not an integer from 1 to 0xff.
134
+ */
135
+ read_bit(offset: number, mask: number): boolean;
125
136
  /**
126
137
  * Writes the bits selected by mask of the byte at offset; the other bits
127
138
  * are unchanged (doc/spec-data-api.md §2.5).
package/package.json CHANGED
@@ -1,30 +1,32 @@
1
1
  {
2
2
  "name": "layout-data",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Structured, reactive, buffer-based data blocks with named, typed tags",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/types/index.d.ts",
9
- "default": "./dist/layout-data.js"
9
+ "default": "./src/index.js"
10
10
  }
11
11
  },
12
12
  "files": [
13
- "dist",
13
+ "src",
14
+ "dist/types",
14
15
  "CHANGELOG.md"
15
16
  ],
16
17
  "engines": {
17
18
  "node": ">=20.11"
18
19
  },
20
+ "dependencies": {
21
+ "yaml": "^2.9.1"
22
+ },
19
23
  "devDependencies": {
20
24
  "@eslint/js": "10.0.1",
21
25
  "@stylistic/eslint-plugin": "5.10.0",
22
26
  "@types/node": "^20.19.43",
23
27
  "eslint": "10.12.0",
24
28
  "globals": "17.13.0",
25
- "rolldown": "^1.2.12",
26
- "typescript": "^5.9.3",
27
- "yaml": "^2.9.1"
29
+ "typescript": "^5.9.3"
28
30
  },
29
31
  "keywords": [
30
32
  "buffer",
@@ -36,11 +38,11 @@
36
38
  "author": "Goosy Jo <goosy.jo@gmail.com>",
37
39
  "repository": {
38
40
  "type": "git",
39
- "url": "git+https://github.com/goosy/data_block.git"
41
+ "url": "git+https://github.com/goosy/layout-data.git"
40
42
  },
41
43
  "license": "MPL-2.0",
42
44
  "scripts": {
43
- "build": "rolldown -c",
45
+ "build": "tsc -p tsconfig.json",
44
46
  "typecheck": "tsc -p tsconfig.json --noEmit false --emitDeclarationOnly --outDir .local/types",
45
47
  "test": "pnpm build && pnpm test:types && node --test",
46
48
  "test:types": "tsc -p type_check",
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
+ }