@fulcro/types 0.1.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/LICENSE +15 -15
- package/README.md +86 -66
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/struct/arithmetic.d.ts +131 -0
- package/dist/struct/arithmetic.js +20 -0
- package/dist/struct/codec.d.ts +58 -0
- package/dist/struct/codec.js +325 -0
- package/dist/struct/index.d.ts +206 -0
- package/dist/struct/index.js +267 -0
- package/package.json +71 -71
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.struct = void 0;
|
|
4
|
+
const codec_1 = require("./codec.js");
|
|
5
|
+
/**
|
|
6
|
+
* Keys an object lists before every other, in numeric order, whatever order
|
|
7
|
+
* they were written in. A field named like one would silently move, so it is
|
|
8
|
+
* refused.
|
|
9
|
+
*/
|
|
10
|
+
const ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
|
|
11
|
+
/**
|
|
12
|
+
* Describes a value for an error message.
|
|
13
|
+
*
|
|
14
|
+
* @param value Value being reported.
|
|
15
|
+
* @returns Its kind.
|
|
16
|
+
*/
|
|
17
|
+
const describeKind = (value) => value === null ? 'null' : typeof value;
|
|
18
|
+
/**
|
|
19
|
+
* Re-throws the error a field's own conversion raised, naming the field.
|
|
20
|
+
*
|
|
21
|
+
* @param name Name of the struct.
|
|
22
|
+
* @param key Field being converted.
|
|
23
|
+
* @param error What the conversion threw.
|
|
24
|
+
* @returns Never.
|
|
25
|
+
*/
|
|
26
|
+
const rethrowForField = (name, key, error) => {
|
|
27
|
+
const message = (original) => `${name}.from: field '${key}': ${original.message}`;
|
|
28
|
+
if (error instanceof RangeError) {
|
|
29
|
+
throw new RangeError(message(error), { cause: error });
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof TypeError) {
|
|
32
|
+
throw new TypeError(message(error), { cause: error });
|
|
33
|
+
}
|
|
34
|
+
if (error instanceof SyntaxError) {
|
|
35
|
+
throw new SyntaxError(message(error), { cause: error });
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Builds the prototype every value of a struct with methods is made on.
|
|
41
|
+
*
|
|
42
|
+
* The methods sit on it, not on each value: a value is its fields and nothing
|
|
43
|
+
* else of its own, so a thousand values cost no function object each, and
|
|
44
|
+
* `Object.keys` still lists exactly the fields that `is` counts. The methods
|
|
45
|
+
* are not enumerable, so spreading a value copies its fields and not them.
|
|
46
|
+
*
|
|
47
|
+
* @param name Name of the struct.
|
|
48
|
+
* @param fields Its fields, which no method may be named like.
|
|
49
|
+
* @param methods The methods.
|
|
50
|
+
* @returns The prototype, frozen.
|
|
51
|
+
* @throws {TypeError} When `methods` is not an object, a method is not a
|
|
52
|
+
* function, or a method is named like a field, an array index or `~layout`.
|
|
53
|
+
*/
|
|
54
|
+
const methodPrototype = (name, fields, methods) => {
|
|
55
|
+
if (typeof methods !== 'object' || methods === null) {
|
|
56
|
+
throw new TypeError(`struct ${name}: expected an object of methods, received ${describeKind(methods)}.`);
|
|
57
|
+
}
|
|
58
|
+
const prototype = {};
|
|
59
|
+
for (const key of Reflect.ownKeys(methods)) {
|
|
60
|
+
const method = methods[key];
|
|
61
|
+
const label = String(key);
|
|
62
|
+
if (typeof key === 'string' && Object.hasOwn(fields, key)) {
|
|
63
|
+
throw new TypeError(`struct ${name}: method '${label}' has the name of a field; a value could not hold both.`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof key === 'string' &&
|
|
66
|
+
(ARRAY_INDEX.test(key) || key === '~layout')) {
|
|
67
|
+
throw new TypeError(`struct ${name}: '${label}' cannot name a method; an array index would be reordered, and '~layout' is the layout itself.`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof method !== 'function') {
|
|
70
|
+
throw new TypeError(`struct ${name}: method '${label}' must be a function, received ${describeKind(method)}.`);
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(prototype, key, {
|
|
73
|
+
value: method,
|
|
74
|
+
enumerable: false,
|
|
75
|
+
writable: false,
|
|
76
|
+
configurable: false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze(prototype);
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Declares a struct: a value type with a fixed layout.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* export const Vector3 = struct('Vector3', {
|
|
86
|
+
* x: SinglePrecisionFloat,
|
|
87
|
+
* y: SinglePrecisionFloat,
|
|
88
|
+
* z: SinglePrecisionFloat,
|
|
89
|
+
* });
|
|
90
|
+
* export type Vector3 = Struct<typeof Vector3>;
|
|
91
|
+
*
|
|
92
|
+
* const up: Vector3 = Vector3.from({ x: 0, y: 1, z: 0 });
|
|
93
|
+
*
|
|
94
|
+
* Vector3.layout.size; // 12
|
|
95
|
+
* Vector3.write(new DataView(buffer), 0, up);
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* A value has no identity: it is frozen, two values with the same fields are
|
|
99
|
+
* equal by {@link StructType.equals}, and it can be written into bytes and
|
|
100
|
+
* read back as the same value. It is still a JavaScript object while it is
|
|
101
|
+
* held as one; the layout is what it occupies when it is stored.
|
|
102
|
+
*
|
|
103
|
+
* Methods, when given, are shared by every value through one prototype: they
|
|
104
|
+
* take no bytes and are not fields, so the layout, `equals` and the bytes are
|
|
105
|
+
* the same as without them. `this` is the value, which is frozen — a method
|
|
106
|
+
* that changes something returns a new value:
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* const Vector3 = struct('Vector3', { x: SinglePrecisionFloat, … }, {
|
|
110
|
+
* length() {
|
|
111
|
+
* return Math.hypot(this.x, this.y, this.z);
|
|
112
|
+
* },
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* Vector3.from({ x: 3, y: 4, z: 0 }).length(); // 5
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @param name Name of the struct, for error messages.
|
|
119
|
+
* @param fields Descriptor of each field, by name.
|
|
120
|
+
* @param methods Function of each method, by name.
|
|
121
|
+
* @returns The descriptor of the struct.
|
|
122
|
+
* @throws {TypeError} When there is no field, a field's type has no fixed
|
|
123
|
+
* layout, a field is named like an array index or `~layout`, or a method is
|
|
124
|
+
* not a function or is named like a field, an array index or `~layout`.
|
|
125
|
+
*/
|
|
126
|
+
const struct = (name, fields, methods) => {
|
|
127
|
+
if (typeof name !== 'string' || name === '') {
|
|
128
|
+
throw new TypeError(`struct: expected a name, received ${describeKind(name)}.`);
|
|
129
|
+
}
|
|
130
|
+
if (typeof fields !== 'object' || fields === null) {
|
|
131
|
+
throw new TypeError(`struct ${name}: expected an object of fields, received ${describeKind(fields)}.`);
|
|
132
|
+
}
|
|
133
|
+
const keys = Object.keys(fields);
|
|
134
|
+
if (keys.length === 0) {
|
|
135
|
+
throw new TypeError(`struct ${name}: expected at least one field.`);
|
|
136
|
+
}
|
|
137
|
+
const declared = keys.map((key) => {
|
|
138
|
+
if (ARRAY_INDEX.test(key) || key === '~layout') {
|
|
139
|
+
throw new TypeError(`struct ${name}: '${key}' cannot name a field; an array index would be reordered, and '~layout' is the layout itself.`);
|
|
140
|
+
}
|
|
141
|
+
const codec = (0, codec_1.codecOf)(fields[key]);
|
|
142
|
+
if (codec === undefined) {
|
|
143
|
+
throw new TypeError(`struct ${name}: field '${key}' has no fixed layout. Declare it with a numeric type of @fulcro/types other than BigInteger, or with another struct.`);
|
|
144
|
+
}
|
|
145
|
+
return { key, descriptor: fields[key], codec };
|
|
146
|
+
});
|
|
147
|
+
const prototype = methods === undefined ? undefined : methodPrototype(name, fields, methods);
|
|
148
|
+
/**
|
|
149
|
+
* Makes the object a value is built on: one carrying the methods, when the
|
|
150
|
+
* struct has any, and a plain one otherwise — so a struct without methods
|
|
151
|
+
* makes exactly the values it made before methods existed.
|
|
152
|
+
*
|
|
153
|
+
* @returns The object, still empty and not yet frozen.
|
|
154
|
+
*/
|
|
155
|
+
const blank = () => prototype === undefined ? {} : Object.create(prototype);
|
|
156
|
+
// Largest alignment first, so that every field lands aligned without padding
|
|
157
|
+
// before it: each size is a multiple of its own alignment. `sort` is stable,
|
|
158
|
+
// which keeps declaration order among equals.
|
|
159
|
+
const offsets = new Map();
|
|
160
|
+
let end = 0;
|
|
161
|
+
for (const field of [...declared].sort((left, right) => right.codec.alignment - left.codec.alignment)) {
|
|
162
|
+
offsets.set(field.key, end);
|
|
163
|
+
end += field.codec.size;
|
|
164
|
+
}
|
|
165
|
+
const alignment = Math.max(...declared.map((field) => field.codec.alignment));
|
|
166
|
+
const size = Math.ceil(end / alignment) * alignment;
|
|
167
|
+
const plan = declared.map((field) => ({
|
|
168
|
+
...field,
|
|
169
|
+
descriptor: field.descriptor,
|
|
170
|
+
offset: offsets.get(field.key),
|
|
171
|
+
}));
|
|
172
|
+
const layout = Object.freeze({
|
|
173
|
+
size,
|
|
174
|
+
alignment,
|
|
175
|
+
fields: Object.freeze(Object.fromEntries(plan.map((field) => [
|
|
176
|
+
field.key,
|
|
177
|
+
Object.freeze({
|
|
178
|
+
offset: field.offset,
|
|
179
|
+
size: field.codec.size,
|
|
180
|
+
alignment: field.codec.alignment,
|
|
181
|
+
}),
|
|
182
|
+
]))),
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Refuses a view and an offset the struct does not fit in, before a byte is
|
|
186
|
+
* touched — so a failed write leaves the view as it was.
|
|
187
|
+
*
|
|
188
|
+
* @param operation Operation being performed.
|
|
189
|
+
* @param view View handed in.
|
|
190
|
+
* @param offset Offset handed in.
|
|
191
|
+
*/
|
|
192
|
+
const requireRoom = (operation, view, offset) => {
|
|
193
|
+
if (!(view instanceof DataView)) {
|
|
194
|
+
throw new TypeError(`${name}.${operation}: expected a DataView, received ${describeKind(view)}.`);
|
|
195
|
+
}
|
|
196
|
+
if (!Number.isInteger(offset) ||
|
|
197
|
+
offset < 0 ||
|
|
198
|
+
offset + size > view.byteLength) {
|
|
199
|
+
throw new RangeError(`${name}.${operation}: ${size} bytes at offset ${offset} do not fit in a view of ${view.byteLength} bytes.`);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const readFields = (view, offset) => {
|
|
203
|
+
const value = blank();
|
|
204
|
+
for (const field of plan) {
|
|
205
|
+
value[field.key] = field.codec.read(view, offset + field.offset);
|
|
206
|
+
}
|
|
207
|
+
return Object.freeze(value);
|
|
208
|
+
};
|
|
209
|
+
const writeFields = (view, offset, value) => {
|
|
210
|
+
for (const field of plan) {
|
|
211
|
+
field.codec.write(view, offset + field.offset, value[field.key]);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const equals = (left, right) => plan.every((field) => field.codec.equals(left[field.key], right[field.key]));
|
|
215
|
+
const descriptor = {
|
|
216
|
+
name,
|
|
217
|
+
layout: layout,
|
|
218
|
+
from: (source) => {
|
|
219
|
+
if (typeof source !== 'object' || source === null) {
|
|
220
|
+
throw new TypeError(`${name}.from: expected an object, received ${describeKind(source)}.`);
|
|
221
|
+
}
|
|
222
|
+
for (const key of Object.keys(source)) {
|
|
223
|
+
if (!Object.hasOwn(fields, key)) {
|
|
224
|
+
throw new TypeError(`${name}.from: '${key}' is not a field; the fields are ${keys.join(', ')}.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const value = blank();
|
|
228
|
+
for (const field of plan) {
|
|
229
|
+
if (!Object.hasOwn(source, field.key)) {
|
|
230
|
+
throw new TypeError(`${name}.from: missing field '${field.key}'.`);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
value[field.key] = field.descriptor.from(source[field.key]);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
rethrowForField(name, field.key, error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return Object.freeze(value);
|
|
240
|
+
},
|
|
241
|
+
is: (value) => typeof value === 'object' &&
|
|
242
|
+
value !== null &&
|
|
243
|
+
(prototype === undefined || Object.getPrototypeOf(value) === prototype) &&
|
|
244
|
+
Object.isFrozen(value) &&
|
|
245
|
+
Object.keys(value).length === plan.length &&
|
|
246
|
+
plan.every((field) => Object.hasOwn(value, field.key) &&
|
|
247
|
+
field.descriptor.is(value[field.key])),
|
|
248
|
+
equals,
|
|
249
|
+
read: (view, offset) => {
|
|
250
|
+
requireRoom('read', view, offset);
|
|
251
|
+
return readFields(view, offset);
|
|
252
|
+
},
|
|
253
|
+
write: (view, offset, value) => {
|
|
254
|
+
requireRoom('write', view, offset);
|
|
255
|
+
writeFields(view, offset, value);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
(0, codec_1.registerStructCodec)(descriptor, {
|
|
259
|
+
size,
|
|
260
|
+
alignment,
|
|
261
|
+
read: readFields,
|
|
262
|
+
write: writeFields,
|
|
263
|
+
equals,
|
|
264
|
+
});
|
|
265
|
+
return descriptor;
|
|
266
|
+
};
|
|
267
|
+
exports.struct = struct;
|
package/package.json
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@fulcro/types",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Numeric types with a defined range and layout: fixed-width integers, half, single and double precision floats, and a decimal128 Decimal.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"integer",
|
|
7
|
-
"float",
|
|
8
|
-
"decimal",
|
|
9
|
-
"decimal128",
|
|
10
|
-
"numeric",
|
|
11
|
-
"typescript"
|
|
12
|
-
],
|
|
13
|
-
"license": "ISC",
|
|
14
|
-
"author": "diguu <rodrigogeribola@hotmail.com>",
|
|
15
|
-
"main": "./dist/index.js",
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
"types": "./dist/index.d.ts",
|
|
20
|
-
"default": "./dist/index.js"
|
|
21
|
-
},
|
|
22
|
-
"./transformer": {
|
|
23
|
-
"types": "./dist/transformer/index.d.ts",
|
|
24
|
-
"default": "./dist/transformer/index.js"
|
|
25
|
-
},
|
|
26
|
-
"./unplugin": {
|
|
27
|
-
"types": "./dist/unplugin/index.d.mts",
|
|
28
|
-
"default": "./dist/unplugin/index.mjs"
|
|
29
|
-
},
|
|
30
|
-
"./language-service": {
|
|
31
|
-
"types": "./dist/languageService/index.d.ts",
|
|
32
|
-
"default": "./dist/languageService/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./package.json": "./package.json"
|
|
35
|
-
},
|
|
36
|
-
"files": [
|
|
37
|
-
"dist"
|
|
38
|
-
],
|
|
39
|
-
"sideEffects": false,
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@fulcro/transform-core": "^0.10.0"
|
|
42
|
-
},
|
|
43
|
-
"peerDependencies": {
|
|
44
|
-
"typescript": ">=5.3.3 <7"
|
|
45
|
-
},
|
|
46
|
-
"peerDependenciesMeta": {
|
|
47
|
-
"typescript": {
|
|
48
|
-
"optional": true
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"engines": {
|
|
52
|
-
"node": ">=22"
|
|
53
|
-
},
|
|
54
|
-
"publishConfig": {
|
|
55
|
-
"access": "public"
|
|
56
|
-
},
|
|
57
|
-
"repository": {
|
|
58
|
-
"type": "git",
|
|
59
|
-
"url": "git+https://github.com/DigUu-RL/fulcro.git",
|
|
60
|
-
"directory": "packages/types"
|
|
61
|
-
},
|
|
62
|
-
"homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/types#readme",
|
|
63
|
-
"bugs": {
|
|
64
|
-
"url": "https://github.com/DigUu-RL/fulcro/issues"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
68
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
69
|
-
"prepublishOnly": "npm run build"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@fulcro/types",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Numeric types with a defined range and layout: fixed-width integers, half, single and double precision floats, and a decimal128 Decimal.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"integer",
|
|
7
|
+
"float",
|
|
8
|
+
"decimal",
|
|
9
|
+
"decimal128",
|
|
10
|
+
"numeric",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "diguu <rodrigogeribola@hotmail.com>",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./transformer": {
|
|
23
|
+
"types": "./dist/transformer/index.d.ts",
|
|
24
|
+
"default": "./dist/transformer/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./unplugin": {
|
|
27
|
+
"types": "./dist/unplugin/index.d.mts",
|
|
28
|
+
"default": "./dist/unplugin/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./language-service": {
|
|
31
|
+
"types": "./dist/languageService/index.d.ts",
|
|
32
|
+
"default": "./dist/languageService/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@fulcro/transform-core": "^0.10.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"typescript": ">=5.3.3 <7"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"typescript": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/DigUu-RL/fulcro.git",
|
|
60
|
+
"directory": "packages/types"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/types#readme",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/DigUu-RL/fulcro/issues"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
68
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
69
|
+
"prepublishOnly": "npm run build"
|
|
70
|
+
}
|
|
71
|
+
}
|