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 +30 -0
- package/README.md +1 -3
- package/dist/types/Ldata.d.ts +16 -5
- package/package.json +10 -8
- package/src/Ldata.js +541 -0
- package/src/Ltag.js +87 -0
- package/src/codecs.js +64 -0
- package/src/dispatch.js +23 -0
- package/src/endian.js +197 -0
- package/src/index.js +8 -0
- package/src/internal_types.js +47 -0
- package/src/layout.js +303 -0
- package/src/observer.js +147 -0
- package/src/registry.js +83 -0
- package/src/reserved.js +35 -0
- package/src/structure.js +238 -0
- package/src/typedefs.js +143 -0
- package/src/types.js +74 -0
- package/dist/layout-data.js +0 -8320
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 #
|
|
115
|
+
pnpm build # generate dist/types
|
|
118
116
|
pnpm test # build, type tests, node --test
|
|
119
117
|
pnpm lint
|
|
120
118
|
```
|
package/dist/types/Ldata.d.ts
CHANGED
|
@@ -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
|
|
95
|
-
* during it; when fn returns,
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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.
|
|
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": "./
|
|
9
|
+
"default": "./src/index.js"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"
|
|
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
|
-
"
|
|
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/
|
|
41
|
+
"url": "git+https://github.com/goosy/layout-data.git"
|
|
40
42
|
},
|
|
41
43
|
"license": "MPL-2.0",
|
|
42
44
|
"scripts": {
|
|
43
|
-
"build": "
|
|
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
|
+
}
|