@fulcro/types 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/dist/struct/index.d.ts +46 -14
- package/dist/struct/index.js +73 -4
- package/package.json +1 -1
package/dist/struct/index.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ interface FieldDescriptor {
|
|
|
12
12
|
type StructFields = {
|
|
13
13
|
readonly [field: string]: FieldDescriptor;
|
|
14
14
|
};
|
|
15
|
+
/** The methods of a struct, by name. */
|
|
16
|
+
type StructMethods = {
|
|
17
|
+
readonly [method: string]: (...parameters: never[]) => unknown;
|
|
18
|
+
};
|
|
19
|
+
/** What a struct declared without methods has: none. */
|
|
20
|
+
type NoMethods = Record<never, never>;
|
|
15
21
|
/** The type of the values a descriptor recognises. */
|
|
16
22
|
type ValueOf<TDescriptor> = TDescriptor extends {
|
|
17
23
|
is(value: unknown): value is infer T;
|
|
@@ -44,10 +50,16 @@ type StructAlignment<TFields extends StructFields> = LargestAlignment<{
|
|
|
44
50
|
* not promise an order for its keys.
|
|
45
51
|
*/
|
|
46
52
|
type StructSize<TFields extends StructFields> = RoundUp<SumOfSizes<TFields, UnionToTuple<keyof TFields>>, StructAlignment<TFields>>;
|
|
47
|
-
/**
|
|
48
|
-
type
|
|
53
|
+
/** The fields of a value of a struct, and its layout. */
|
|
54
|
+
type StructData<TFields extends StructFields> = {
|
|
49
55
|
readonly [TKey in keyof TFields]: ValueOf<TFields[TKey]>;
|
|
50
56
|
} & Layout<StructSize<TFields>, StructAlignment<TFields>>;
|
|
57
|
+
/**
|
|
58
|
+
* A value of a struct: its fields, and its methods when it declares any. A
|
|
59
|
+
* struct without methods is its fields alone, exactly as before methods
|
|
60
|
+
* existed, so nothing it inferred changes.
|
|
61
|
+
*/
|
|
62
|
+
type StructValue<TFields extends StructFields, TMethods extends StructMethods = NoMethods> = [keyof TMethods] extends [never] ? StructData<TFields> : StructData<TFields> & Readonly<TMethods>;
|
|
51
63
|
/** What a struct's `from` accepts: each field in what its own `from` accepts. */
|
|
52
64
|
type StructSource<TFields extends StructFields> = {
|
|
53
65
|
readonly [TKey in keyof TFields]: SourceOf<TFields[TKey]>;
|
|
@@ -57,8 +69,9 @@ type StructSource<TFields extends StructFields> = {
|
|
|
57
69
|
* recognised, compared and stored.
|
|
58
70
|
*
|
|
59
71
|
* @template TFields Descriptors of the fields, by name.
|
|
72
|
+
* @template TMethods Methods every value carries, by name; none by default.
|
|
60
73
|
*/
|
|
61
|
-
export interface StructType<TFields extends StructFields> {
|
|
74
|
+
export interface StructType<TFields extends StructFields, TMethods extends StructMethods = NoMethods> {
|
|
62
75
|
/** Name of the struct, as it reads in an error message. */
|
|
63
76
|
readonly name: string;
|
|
64
77
|
/**
|
|
@@ -85,20 +98,22 @@ export interface StructType<TFields extends StructFields> {
|
|
|
85
98
|
* Makes a value, converting each field with its own type's `from`.
|
|
86
99
|
*
|
|
87
100
|
* @param source One entry per field, and nothing else.
|
|
88
|
-
* @returns The value, frozen.
|
|
101
|
+
* @returns The value, frozen, carrying the struct's methods.
|
|
89
102
|
* @throws {TypeError} When a field is missing or not a field of the struct.
|
|
90
103
|
* @throws {RangeError} When a field's own conversion refuses its value; the
|
|
91
104
|
* message names the field, and `cause` is the original error.
|
|
92
105
|
*/
|
|
93
|
-
from(source: StructSource<TFields>): StructValue<TFields>;
|
|
106
|
+
from(source: StructSource<TFields>): StructValue<TFields, TMethods>;
|
|
94
107
|
/**
|
|
95
108
|
* Tells whether a value is one of this struct's: frozen, with exactly its
|
|
96
|
-
* fields, each of its type
|
|
109
|
+
* fields, each of its type — and, when the struct declares methods, made by
|
|
110
|
+
* this struct, since an object with the right fields alone would not carry
|
|
111
|
+
* them.
|
|
97
112
|
*
|
|
98
113
|
* @param value Value to inspect.
|
|
99
114
|
* @returns `true` when it is.
|
|
100
115
|
*/
|
|
101
|
-
is(value: unknown): value is StructValue<TFields>;
|
|
116
|
+
is(value: unknown): value is StructValue<TFields, TMethods>;
|
|
102
117
|
/**
|
|
103
118
|
* Compares two values field by field, each as its own type compares — so a
|
|
104
119
|
* `NaN` field makes a value unequal to itself, as it does in C#.
|
|
@@ -107,18 +122,18 @@ export interface StructType<TFields extends StructFields> {
|
|
|
107
122
|
* @param right Second value.
|
|
108
123
|
* @returns `true` when every field is equal.
|
|
109
124
|
*/
|
|
110
|
-
equals(left: StructValue<TFields>, right: StructValue<TFields>): boolean;
|
|
125
|
+
equals(left: StructValue<TFields, TMethods>, right: StructValue<TFields, TMethods>): boolean;
|
|
111
126
|
/**
|
|
112
127
|
* Reads a value from bytes, in the layout of {@link StructType.layout},
|
|
113
128
|
* little-endian.
|
|
114
129
|
*
|
|
115
130
|
* @param view Bytes to read from.
|
|
116
131
|
* @param offset Where the value starts.
|
|
117
|
-
* @returns The value, frozen.
|
|
132
|
+
* @returns The value, frozen, carrying the struct's methods.
|
|
118
133
|
* @throws {RangeError} When the struct does not fit in the view at that
|
|
119
134
|
* offset.
|
|
120
135
|
*/
|
|
121
|
-
read(view: DataView, offset: number): StructValue<TFields>;
|
|
136
|
+
read(view: DataView, offset: number): StructValue<TFields, TMethods>;
|
|
122
137
|
/**
|
|
123
138
|
* Writes a value into bytes, in the layout of {@link StructType.layout},
|
|
124
139
|
* little-endian. Padding bytes are left as they were.
|
|
@@ -129,7 +144,7 @@ export interface StructType<TFields extends StructFields> {
|
|
|
129
144
|
* @throws {RangeError} When the struct does not fit in the view at that
|
|
130
145
|
* offset.
|
|
131
146
|
*/
|
|
132
|
-
write(view: DataView, offset: number, value: StructValue<TFields>): void;
|
|
147
|
+
write(view: DataView, offset: number, value: StructValue<TFields, TMethods>): void;
|
|
133
148
|
}
|
|
134
149
|
/**
|
|
135
150
|
* The type of the values of a struct, named from its descriptor.
|
|
@@ -141,7 +156,7 @@ export interface StructType<TFields extends StructFields> {
|
|
|
141
156
|
*
|
|
142
157
|
* @template TDescriptor Type of the descriptor `struct` returned.
|
|
143
158
|
*/
|
|
144
|
-
export type Struct<TDescriptor extends StructType<StructFields>> = TDescriptor extends StructType<infer TFields> ? StructValue<TFields> : never;
|
|
159
|
+
export type Struct<TDescriptor extends StructType<StructFields, StructMethods>> = TDescriptor extends StructType<infer TFields, infer TMethods> ? StructValue<TFields, TMethods> : never;
|
|
145
160
|
/**
|
|
146
161
|
* Declares a struct: a value type with a fixed layout.
|
|
147
162
|
*
|
|
@@ -164,11 +179,28 @@ export type Struct<TDescriptor extends StructType<StructFields>> = TDescriptor e
|
|
|
164
179
|
* read back as the same value. It is still a JavaScript object while it is
|
|
165
180
|
* held as one; the layout is what it occupies when it is stored.
|
|
166
181
|
*
|
|
182
|
+
* Methods, when given, are shared by every value through one prototype: they
|
|
183
|
+
* take no bytes and are not fields, so the layout, `equals` and the bytes are
|
|
184
|
+
* the same as without them. `this` is the value, which is frozen — a method
|
|
185
|
+
* that changes something returns a new value:
|
|
186
|
+
*
|
|
187
|
+
* ```ts
|
|
188
|
+
* const Vector3 = struct('Vector3', { x: SinglePrecisionFloat, … }, {
|
|
189
|
+
* length() {
|
|
190
|
+
* return Math.hypot(this.x, this.y, this.z);
|
|
191
|
+
* },
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* Vector3.from({ x: 3, y: 4, z: 0 }).length(); // 5
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
167
197
|
* @param name Name of the struct, for error messages.
|
|
168
198
|
* @param fields Descriptor of each field, by name.
|
|
199
|
+
* @param methods Function of each method, by name.
|
|
169
200
|
* @returns The descriptor of the struct.
|
|
170
201
|
* @throws {TypeError} When there is no field, a field's type has no fixed
|
|
171
|
-
* layout,
|
|
202
|
+
* layout, a field is named like an array index or `~layout`, or a method is
|
|
203
|
+
* not a function or is named like a field, an array index or `~layout`.
|
|
172
204
|
*/
|
|
173
|
-
export declare const struct: <TFields extends StructFields>(name: string, fields: TFields) => StructType<TFields>;
|
|
205
|
+
export declare const struct: <TFields extends StructFields, TMethods extends StructMethods = NoMethods>(name: string, fields: TFields, methods?: TMethods & ThisType<StructValue<TFields, TMethods>>) => StructType<TFields, TMethods>;
|
|
174
206
|
export {};
|
package/dist/struct/index.js
CHANGED
|
@@ -36,6 +36,48 @@ const rethrowForField = (name, key, error) => {
|
|
|
36
36
|
}
|
|
37
37
|
throw error;
|
|
38
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
|
+
};
|
|
39
81
|
/**
|
|
40
82
|
* Declares a struct: a value type with a fixed layout.
|
|
41
83
|
*
|
|
@@ -58,13 +100,30 @@ const rethrowForField = (name, key, error) => {
|
|
|
58
100
|
* read back as the same value. It is still a JavaScript object while it is
|
|
59
101
|
* held as one; the layout is what it occupies when it is stored.
|
|
60
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
|
+
*
|
|
61
118
|
* @param name Name of the struct, for error messages.
|
|
62
119
|
* @param fields Descriptor of each field, by name.
|
|
120
|
+
* @param methods Function of each method, by name.
|
|
63
121
|
* @returns The descriptor of the struct.
|
|
64
122
|
* @throws {TypeError} When there is no field, a field's type has no fixed
|
|
65
|
-
* layout,
|
|
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`.
|
|
66
125
|
*/
|
|
67
|
-
const struct = (name, fields) => {
|
|
126
|
+
const struct = (name, fields, methods) => {
|
|
68
127
|
if (typeof name !== 'string' || name === '') {
|
|
69
128
|
throw new TypeError(`struct: expected a name, received ${describeKind(name)}.`);
|
|
70
129
|
}
|
|
@@ -85,6 +144,15 @@ const struct = (name, fields) => {
|
|
|
85
144
|
}
|
|
86
145
|
return { key, descriptor: fields[key], codec };
|
|
87
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);
|
|
88
156
|
// Largest alignment first, so that every field lands aligned without padding
|
|
89
157
|
// before it: each size is a multiple of its own alignment. `sort` is stable,
|
|
90
158
|
// which keeps declaration order among equals.
|
|
@@ -132,7 +200,7 @@ const struct = (name, fields) => {
|
|
|
132
200
|
}
|
|
133
201
|
};
|
|
134
202
|
const readFields = (view, offset) => {
|
|
135
|
-
const value =
|
|
203
|
+
const value = blank();
|
|
136
204
|
for (const field of plan) {
|
|
137
205
|
value[field.key] = field.codec.read(view, offset + field.offset);
|
|
138
206
|
}
|
|
@@ -156,7 +224,7 @@ const struct = (name, fields) => {
|
|
|
156
224
|
throw new TypeError(`${name}.from: '${key}' is not a field; the fields are ${keys.join(', ')}.`);
|
|
157
225
|
}
|
|
158
226
|
}
|
|
159
|
-
const value =
|
|
227
|
+
const value = blank();
|
|
160
228
|
for (const field of plan) {
|
|
161
229
|
if (!Object.hasOwn(source, field.key)) {
|
|
162
230
|
throw new TypeError(`${name}.from: missing field '${field.key}'.`);
|
|
@@ -172,6 +240,7 @@ const struct = (name, fields) => {
|
|
|
172
240
|
},
|
|
173
241
|
is: (value) => typeof value === 'object' &&
|
|
174
242
|
value !== null &&
|
|
243
|
+
(prototype === undefined || Object.getPrototypeOf(value) === prototype) &&
|
|
175
244
|
Object.isFrozen(value) &&
|
|
176
245
|
Object.keys(value).length === plan.length &&
|
|
177
246
|
plan.every((field) => Object.hasOwn(value, field.key) &&
|
package/package.json
CHANGED