@axi-engine/fields 0.1.5 → 0.2.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/README.md +13 -7
- package/dist/index.d.mts +648 -212
- package/dist/index.d.ts +648 -212
- package/dist/index.js +707 -382
- package/dist/index.mjs +688 -373
- package/package.json +3 -6
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
FieldsNodeType2["fieldTree"] = "FieldTree";
|
|
4
|
-
FieldsNodeType2["fields"] = "Fields";
|
|
5
|
-
return FieldsNodeType2;
|
|
6
|
-
})(FieldsNodeType || {});
|
|
7
|
-
|
|
8
|
-
// src/field-policies.ts
|
|
9
|
-
var _ClampPolicy = class _ClampPolicy {
|
|
1
|
+
// src/policies/clamp-policy.ts
|
|
2
|
+
var ClampPolicy = class _ClampPolicy {
|
|
10
3
|
constructor(min, max) {
|
|
11
4
|
this.min = min;
|
|
12
5
|
this.max = max;
|
|
13
|
-
this.id = _ClampPolicy.id;
|
|
14
6
|
}
|
|
7
|
+
static id = "clamp";
|
|
8
|
+
id = _ClampPolicy.id;
|
|
15
9
|
apply(val) {
|
|
16
10
|
return Math.max(this.min, Math.min(this.max, val));
|
|
17
11
|
}
|
|
@@ -20,27 +14,17 @@ var _ClampPolicy = class _ClampPolicy {
|
|
|
20
14
|
this.max = max;
|
|
21
15
|
}
|
|
22
16
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
apply(val) {
|
|
31
|
-
return Math.max(this.min, val);
|
|
32
|
-
}
|
|
33
|
-
updateBounds(min) {
|
|
34
|
-
this.min = min;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
_ClampMinPolicy.id = "clampMin";
|
|
38
|
-
var ClampMinPolicy = _ClampMinPolicy;
|
|
39
|
-
var _ClampMaxPolicy = class _ClampMaxPolicy {
|
|
17
|
+
function clampPolicy(min, max) {
|
|
18
|
+
return new ClampPolicy(min, max);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/policies/clamp-max-policy.ts
|
|
22
|
+
var ClampMaxPolicy = class _ClampMaxPolicy {
|
|
40
23
|
constructor(max) {
|
|
41
24
|
this.max = max;
|
|
42
|
-
this.id = _ClampMaxPolicy.id;
|
|
43
25
|
}
|
|
26
|
+
static id = "clampMax";
|
|
27
|
+
id = _ClampMaxPolicy.id;
|
|
44
28
|
apply(val) {
|
|
45
29
|
return Math.min(this.max, val);
|
|
46
30
|
}
|
|
@@ -48,58 +32,33 @@ var _ClampMaxPolicy = class _ClampMaxPolicy {
|
|
|
48
32
|
this.max = max;
|
|
49
33
|
}
|
|
50
34
|
};
|
|
51
|
-
_ClampMaxPolicy.id = "clampMax";
|
|
52
|
-
var ClampMaxPolicy = _ClampMaxPolicy;
|
|
53
|
-
function clampPolicy(min, max) {
|
|
54
|
-
return new ClampPolicy(min, max);
|
|
55
|
-
}
|
|
56
|
-
function clampMinPolicy(min) {
|
|
57
|
-
return new ClampMinPolicy(min);
|
|
58
|
-
}
|
|
59
35
|
function clampMaxPolicy(max) {
|
|
60
36
|
return new ClampMaxPolicy(max);
|
|
61
37
|
}
|
|
62
38
|
|
|
63
|
-
// src/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* Creates an instance of a Field.
|
|
68
|
-
* @param name A unique identifier for the field.
|
|
69
|
-
* @param initialVal The initial value of the field.
|
|
70
|
-
* @param options Optional configuration for the field.
|
|
71
|
-
* @param options.policies An array of policies to apply to the field's value on every `set` operation.
|
|
72
|
-
*/
|
|
73
|
-
constructor(name, initialVal, options) {
|
|
74
|
-
this.policies = /* @__PURE__ */ new Map();
|
|
75
|
-
this._val = signal(initialVal);
|
|
76
|
-
this.name = name;
|
|
77
|
-
options?.policies?.forEach((policy) => this.policies.set(policy.id, policy));
|
|
78
|
-
this.set(initialVal);
|
|
39
|
+
// src/policies/clamp-min-policy.ts
|
|
40
|
+
var ClampMinPolicy = class _ClampMinPolicy {
|
|
41
|
+
constructor(min) {
|
|
42
|
+
this.min = min;
|
|
79
43
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
get val() {
|
|
85
|
-
return this._val.value;
|
|
44
|
+
static id = "clampMin";
|
|
45
|
+
id = _ClampMinPolicy.id;
|
|
46
|
+
apply(val) {
|
|
47
|
+
return Math.max(this.min, val);
|
|
86
48
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* Subscribe to this signal to react to value changes.
|
|
90
|
-
*/
|
|
91
|
-
get signal() {
|
|
92
|
-
return this._val;
|
|
49
|
+
updateBounds(min) {
|
|
50
|
+
this.min = min;
|
|
93
51
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
52
|
+
};
|
|
53
|
+
function clampMinPolicy(min) {
|
|
54
|
+
return new ClampMinPolicy(min);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/policies/policies.ts
|
|
58
|
+
var Policies = class {
|
|
59
|
+
policies = /* @__PURE__ */ new Map();
|
|
60
|
+
get items() {
|
|
61
|
+
return this.policies;
|
|
103
62
|
}
|
|
104
63
|
/**
|
|
105
64
|
* Retrieves a specific policy instance by its ID.
|
|
@@ -108,7 +67,7 @@ var Field = class {
|
|
|
108
67
|
* @param id The unique ID of the policy to retrieve.
|
|
109
68
|
* @returns The policy instance, or `undefined` if not found.
|
|
110
69
|
*/
|
|
111
|
-
|
|
70
|
+
get(id) {
|
|
112
71
|
return this.policies.get(id);
|
|
113
72
|
}
|
|
114
73
|
/**
|
|
@@ -117,17 +76,18 @@ var Field = class {
|
|
|
117
76
|
* If a policy with the same ID already exists, its `destroy` method will be called before it is replaced.
|
|
118
77
|
* @param policy The policy instance to add.
|
|
119
78
|
*/
|
|
120
|
-
|
|
79
|
+
add(policy) {
|
|
121
80
|
const existed = this.policies.get(policy.id);
|
|
122
81
|
existed?.destroy?.();
|
|
123
82
|
this.policies.set(policy.id, policy);
|
|
83
|
+
return this;
|
|
124
84
|
}
|
|
125
85
|
/**
|
|
126
86
|
* Removes a policy from the field by its ID and call `destroy` method.
|
|
127
87
|
* @param policyId The unique ID of the policy to remove.
|
|
128
88
|
* @returns `true` if the policy was found and removed, otherwise `false`.
|
|
129
89
|
*/
|
|
130
|
-
|
|
90
|
+
remove(policyId) {
|
|
131
91
|
const policyToRemove = this.policies.get(policyId);
|
|
132
92
|
if (!policyToRemove) {
|
|
133
93
|
return false;
|
|
@@ -135,11 +95,14 @@ var Field = class {
|
|
|
135
95
|
policyToRemove.destroy?.();
|
|
136
96
|
return this.policies.delete(policyId);
|
|
137
97
|
}
|
|
98
|
+
isEmpty() {
|
|
99
|
+
return this.policies.size === 0;
|
|
100
|
+
}
|
|
138
101
|
/**
|
|
139
102
|
* Removes all policies from the field.
|
|
140
103
|
* After this, `set()` will no longer apply any transformations to the value until new policies are added.
|
|
141
104
|
*/
|
|
142
|
-
|
|
105
|
+
clear() {
|
|
143
106
|
this.policies.forEach((policy) => policy.destroy?.());
|
|
144
107
|
this.policies.clear();
|
|
145
108
|
}
|
|
@@ -147,37 +110,133 @@ var Field = class {
|
|
|
147
110
|
* Forces the current value to be re-processed by all policies.
|
|
148
111
|
* Useful if a policy's logic has changed and you need to re-evaluate the current state.
|
|
149
112
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
113
|
+
apply(val) {
|
|
114
|
+
let finalVal = val;
|
|
115
|
+
this.policies.forEach((policy) => finalVal = policy.apply(finalVal));
|
|
116
|
+
return finalVal;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/field-definitions/default-field.ts
|
|
121
|
+
import { Emitter } from "@axi-engine/utils";
|
|
122
|
+
import { dequal } from "dequal";
|
|
123
|
+
var DefaultField = class _DefaultField {
|
|
124
|
+
/** A type keyword of the field */
|
|
125
|
+
static typeName = "default";
|
|
126
|
+
typeName = _DefaultField.typeName;
|
|
127
|
+
/** A unique identifier for the field. */
|
|
128
|
+
_name;
|
|
129
|
+
_value;
|
|
130
|
+
_onChange = new Emitter();
|
|
131
|
+
onChange;
|
|
132
|
+
policies = new Policies();
|
|
133
|
+
get name() {
|
|
134
|
+
return this._name;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Gets the current raw value of the field.
|
|
138
|
+
* For reactive updates, it's recommended to use the `.signal` property instead.
|
|
139
|
+
*/
|
|
140
|
+
get value() {
|
|
141
|
+
return this._value;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Sets a new value for the field.
|
|
145
|
+
* The provided value will be processed by all registered policies before the underlying signal is updated.
|
|
146
|
+
* @param val The new value to set.
|
|
147
|
+
*/
|
|
148
|
+
set value(val) {
|
|
149
|
+
const oldVal = this._value;
|
|
150
|
+
const finalVal = this.policies.apply(val);
|
|
151
|
+
if (!dequal(this._value, finalVal)) {
|
|
152
|
+
this._value = finalVal;
|
|
153
|
+
this._onChange.emit(this._value, oldVal);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates an instance of a Field.
|
|
158
|
+
* @param name A unique identifier for the field.
|
|
159
|
+
* @param initialVal The initial value of the field.
|
|
160
|
+
* @param options Optional configuration for the field.
|
|
161
|
+
* @param options.policies An array of policies to apply to the field's value on every `set` operation.
|
|
162
|
+
* @param options.isEqual An function for compare old and new value, by default uses the strictEquals from `utils`
|
|
163
|
+
*
|
|
164
|
+
*/
|
|
165
|
+
constructor(name, initialVal, options) {
|
|
166
|
+
this.onChange = this._onChange;
|
|
167
|
+
this._name = name;
|
|
168
|
+
options?.policies?.forEach((policy) => this.policies.add(policy));
|
|
169
|
+
this.value = initialVal;
|
|
170
|
+
}
|
|
171
|
+
setValueSilently(val) {
|
|
172
|
+
this._value = this.policies.apply(val);
|
|
173
|
+
}
|
|
174
|
+
batchUpdate(updateFn) {
|
|
175
|
+
this.value = updateFn(this.value);
|
|
152
176
|
}
|
|
153
177
|
/**
|
|
154
178
|
* Cleans up resources used by the field and its policies.
|
|
155
179
|
* This should be called when the field is no longer needed to prevent memory leaks from reactive policies.
|
|
156
180
|
*/
|
|
157
181
|
destroy() {
|
|
158
|
-
this.
|
|
182
|
+
this.policies.clear();
|
|
183
|
+
this._onChange.clear();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/field-definitions/default-boolean-field.ts
|
|
188
|
+
var DefaultBooleanField = class _DefaultBooleanField extends DefaultField {
|
|
189
|
+
static typeName = "boolean";
|
|
190
|
+
typeName = _DefaultBooleanField.typeName;
|
|
191
|
+
constructor(name, initialVal, options) {
|
|
192
|
+
super(name, initialVal, options);
|
|
193
|
+
}
|
|
194
|
+
toggle() {
|
|
195
|
+
this.value = !this.value;
|
|
196
|
+
return this.value;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// src/field-definitions/default-string-field.ts
|
|
201
|
+
var DefaultStringField = class _DefaultStringField extends DefaultField {
|
|
202
|
+
static typeName = "string";
|
|
203
|
+
typeName = _DefaultStringField.typeName;
|
|
204
|
+
constructor(name, initialVal, options) {
|
|
205
|
+
super(name, initialVal, options);
|
|
206
|
+
}
|
|
207
|
+
append(str) {
|
|
208
|
+
this.value = this.value + str;
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
prepend(str) {
|
|
212
|
+
this.value = str + this.value;
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
trim() {
|
|
216
|
+
this.value = this.value.trim();
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
isEmpty() {
|
|
220
|
+
return this.value.length === 0;
|
|
221
|
+
}
|
|
222
|
+
clear() {
|
|
223
|
+
this.value = "";
|
|
159
224
|
}
|
|
160
225
|
};
|
|
161
226
|
|
|
162
|
-
// src/
|
|
227
|
+
// src/field-definitions/default-numeric-field.ts
|
|
163
228
|
import { isNullOrUndefined } from "@axi-engine/utils";
|
|
164
|
-
var
|
|
229
|
+
var DefaultNumericField = class _DefaultNumericField extends DefaultField {
|
|
230
|
+
static typeName = "numeric";
|
|
231
|
+
typeName = _DefaultNumericField.typeName;
|
|
165
232
|
get min() {
|
|
166
|
-
const policy = this.
|
|
233
|
+
const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMinPolicy.id);
|
|
167
234
|
return policy?.min;
|
|
168
235
|
}
|
|
169
236
|
get max() {
|
|
170
|
-
const policy = this.
|
|
237
|
+
const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMaxPolicy.id);
|
|
171
238
|
return policy?.max;
|
|
172
239
|
}
|
|
173
|
-
get isMin() {
|
|
174
|
-
const min = this.min;
|
|
175
|
-
return isNullOrUndefined(min) ? false : this.val <= min;
|
|
176
|
-
}
|
|
177
|
-
get isMax() {
|
|
178
|
-
const max = this.max;
|
|
179
|
-
return isNullOrUndefined(max) ? false : this.val >= max;
|
|
180
|
-
}
|
|
181
240
|
constructor(name, initialVal, options) {
|
|
182
241
|
const policies = options?.policies ?? [];
|
|
183
242
|
if (!isNullOrUndefined(options?.min) && !isNullOrUndefined(options?.max)) {
|
|
@@ -189,364 +248,637 @@ var NumberField = class extends Field {
|
|
|
189
248
|
}
|
|
190
249
|
super(name, initialVal, { policies });
|
|
191
250
|
}
|
|
251
|
+
isMin() {
|
|
252
|
+
const min = this.min;
|
|
253
|
+
return isNullOrUndefined(min) ? false : this.value <= min;
|
|
254
|
+
}
|
|
255
|
+
isMax() {
|
|
256
|
+
const max = this.max;
|
|
257
|
+
return isNullOrUndefined(max) ? false : this.value >= max;
|
|
258
|
+
}
|
|
192
259
|
inc(amount = 1) {
|
|
193
|
-
this.
|
|
260
|
+
this.value = this.value + amount;
|
|
194
261
|
}
|
|
195
262
|
dec(amount = 1) {
|
|
196
|
-
this.
|
|
263
|
+
this.value = this.value - amount;
|
|
197
264
|
}
|
|
198
265
|
};
|
|
199
266
|
|
|
200
|
-
// src/
|
|
201
|
-
import {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
267
|
+
// src/utils/constructor-registry.ts
|
|
268
|
+
import { throwIf, throwIfEmpty } from "@axi-engine/utils";
|
|
269
|
+
var ConstructorRegistry = class {
|
|
270
|
+
items = /* @__PURE__ */ new Map();
|
|
271
|
+
/**
|
|
272
|
+
* Registers a constructor with a unique string identifier.
|
|
273
|
+
*
|
|
274
|
+
* @param typeId - The unique identifier for the constructor (e.g., a static `typeName` property from a class).
|
|
275
|
+
* @param ctor - The class constructor to register.
|
|
276
|
+
* @returns The registry instance for chainable calls.
|
|
277
|
+
* @throws If a constructor with the same `typeId` is already registered.
|
|
278
|
+
*/
|
|
279
|
+
register(typeId, ctor) {
|
|
280
|
+
throwIf(this.items.has(typeId), `A constructor with typeId '${typeId}' is already registered.`);
|
|
281
|
+
this.items.set(typeId, ctor);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Retrieves a constructor by its identifier.
|
|
286
|
+
*
|
|
287
|
+
* @param typeId - The identifier of the constructor to retrieve.
|
|
288
|
+
* @returns The found class constructor.
|
|
289
|
+
* @throws If no constructor is found for the given `typeId`.
|
|
290
|
+
*/
|
|
291
|
+
get(typeId) {
|
|
292
|
+
const Ctor = this.items.get(typeId);
|
|
293
|
+
throwIfEmpty(Ctor, `No constructor found for typeId '${typeId}'`);
|
|
294
|
+
return Ctor;
|
|
208
295
|
}
|
|
209
296
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
297
|
+
* Checks if a constructor for a given identifier is registered.
|
|
298
|
+
* @param typeId - The identifier to check.
|
|
299
|
+
* @returns `true` if a constructor is registered, otherwise `false`.
|
|
300
|
+
*/
|
|
301
|
+
has(typeId) {
|
|
302
|
+
return this.items.has(typeId);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Clears all registered constructors from the registry.
|
|
306
|
+
*/
|
|
307
|
+
clear() {
|
|
308
|
+
this.items.clear();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/field-registry.ts
|
|
313
|
+
var FieldRegistry = class extends ConstructorRegistry {
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/fields.ts
|
|
317
|
+
import { Emitter as Emitter2, throwIf as throwIf2 } from "@axi-engine/utils";
|
|
318
|
+
var Fields = class _Fields {
|
|
319
|
+
static typeName = "fields";
|
|
320
|
+
typeName = _Fields.typeName;
|
|
321
|
+
_fields = /* @__PURE__ */ new Map();
|
|
322
|
+
_fieldRegistry;
|
|
323
|
+
/**
|
|
324
|
+
* An event emitter that fires when a new field is added to the collection.
|
|
325
|
+
* @event
|
|
326
|
+
* @param {object} event - The event payload.
|
|
327
|
+
* @param {string} event.name - The name of the added field.
|
|
328
|
+
* @param {Field<any>} event.field - The `Field` instance that was added.
|
|
329
|
+
*/
|
|
330
|
+
onAdd = new Emitter2();
|
|
331
|
+
/**
|
|
332
|
+
* An event emitter that fires after one or more fields have been removed.
|
|
333
|
+
* @event
|
|
334
|
+
* @param {object} event - The event payload.
|
|
335
|
+
* @param {string[]} event.names - An array of names of the fields that were successfully removed.
|
|
336
|
+
*/
|
|
337
|
+
onRemove = new Emitter2();
|
|
338
|
+
/**
|
|
339
|
+
* Gets the read-only map of all `Field` instances in this container.
|
|
340
|
+
* @returns {Map<string, Field<any>>} The collection of fields.
|
|
213
341
|
*/
|
|
214
342
|
get fields() {
|
|
215
343
|
return this._fields;
|
|
216
344
|
}
|
|
217
345
|
/**
|
|
218
|
-
*
|
|
219
|
-
* @param
|
|
220
|
-
* @returns `true` if the field exists, otherwise `false`.
|
|
346
|
+
* Creates an instance of Fields.
|
|
347
|
+
* @param {FieldRegistry} fieldRegistry - The registry used to create new `Field` instances.
|
|
221
348
|
*/
|
|
222
|
-
|
|
223
|
-
|
|
349
|
+
constructor(fieldRegistry) {
|
|
350
|
+
this._fieldRegistry = fieldRegistry;
|
|
224
351
|
}
|
|
225
352
|
/**
|
|
226
|
-
*
|
|
227
|
-
* @param name The
|
|
228
|
-
* @
|
|
229
|
-
* @returns The newly created `Field` instance.
|
|
353
|
+
* Checks if a field with the given name exists in the collection.
|
|
354
|
+
* @param {string} name The name of the field to check.
|
|
355
|
+
* @returns {boolean} `true` if the field exists, otherwise `false`.
|
|
230
356
|
*/
|
|
231
|
-
|
|
232
|
-
return this.
|
|
357
|
+
has(name) {
|
|
358
|
+
return this._fields.has(name);
|
|
233
359
|
}
|
|
234
360
|
/**
|
|
235
|
-
* Adds a pre-existing `Field` instance to the collection.
|
|
236
|
-
*
|
|
237
|
-
* @param field The `Field` instance to add.
|
|
238
|
-
* @returns The added `Field` instance
|
|
361
|
+
* Adds a pre-existing `Field` instance to the collection and fires the `onAdd` event.
|
|
362
|
+
* @template T - The specific `Field` type being added.
|
|
363
|
+
* @param {Field<any>} field - The `Field` instance to add.
|
|
364
|
+
* @returns {T} The added `Field` instance, cast to type `T`.
|
|
365
|
+
* @throws If a field with the same name already exists.
|
|
239
366
|
*/
|
|
240
367
|
add(field) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this.events.emit("created", {
|
|
246
|
-
fieldName: field.name,
|
|
368
|
+
throwIf2(this.has(field.name), `Field with name '${field.name}' already exists`);
|
|
369
|
+
this._fields.set(field.name, field);
|
|
370
|
+
this.onAdd.emit({
|
|
371
|
+
name: field.name,
|
|
247
372
|
field
|
|
248
373
|
});
|
|
249
374
|
return field;
|
|
250
375
|
}
|
|
251
376
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* @
|
|
255
|
-
* @
|
|
377
|
+
* Creates a new `Field` instance of a specified type, adds it to the collection, and returns it.
|
|
378
|
+
* This is the primary factory method for creating fields within this container.
|
|
379
|
+
* @template T - The expected `Field` type to be returned.
|
|
380
|
+
* @param {string} typeName - The registered type name of the field to create (e.g., 'numeric', 'boolean').
|
|
381
|
+
* @param {string} name - The unique name for the new field.
|
|
382
|
+
* @param {*} initialValue - The initial value for the new field.
|
|
383
|
+
* @param {*} [options] - Optional configuration passed to the field's constructor.
|
|
384
|
+
* @returns {T} The newly created `Field` instance.
|
|
256
385
|
*/
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
386
|
+
create(typeName, name, initialValue, options) {
|
|
387
|
+
const Ctor = this._fieldRegistry.get(typeName);
|
|
388
|
+
const field = new Ctor(name, initialValue, options);
|
|
389
|
+
this.add(field);
|
|
390
|
+
return field;
|
|
260
391
|
}
|
|
261
392
|
/**
|
|
262
|
-
*
|
|
263
|
-
* @
|
|
264
|
-
* @param
|
|
265
|
-
* @
|
|
393
|
+
* Updates an existing field's value or creates a new one if it doesn't exist.
|
|
394
|
+
* @template T - The expected `Field` type.
|
|
395
|
+
* @param {string} typeName - The type name to use if a new field needs to be created.
|
|
396
|
+
* @param {string} name - The name of the field to update or create.
|
|
397
|
+
* @param {*} value - The new value to set.
|
|
398
|
+
* @param {*} [options] - Optional configuration, used only if a new field is created.
|
|
399
|
+
* @returns {T} The existing or newly created `Field` instance.
|
|
266
400
|
*/
|
|
267
|
-
upset(name, value) {
|
|
401
|
+
upset(typeName, name, value, options) {
|
|
268
402
|
if (this.has(name)) {
|
|
269
403
|
const field = this.get(name);
|
|
270
|
-
field.
|
|
404
|
+
field.value = value;
|
|
271
405
|
return field;
|
|
272
406
|
}
|
|
273
|
-
return this.create(name, value);
|
|
407
|
+
return this.create(typeName, name, value, options);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Retrieves a field by its name.
|
|
411
|
+
* @template T - The expected `Field` type to be returned.
|
|
412
|
+
* @param {string} name - The name of the field to retrieve.
|
|
413
|
+
* @returns {T} The `Field` instance.
|
|
414
|
+
* @throws If the field does not exist.
|
|
415
|
+
*/
|
|
416
|
+
get(name) {
|
|
417
|
+
throwIf2(!this._fields.has(name), `Field with name '${name}' not exists`);
|
|
418
|
+
return this._fields.get(name);
|
|
274
419
|
}
|
|
275
420
|
/**
|
|
276
421
|
* Removes one or more fields from the collection.
|
|
277
422
|
* This method ensures that the `destroy` method of each removed field is called to clean up its resources.
|
|
278
|
-
* @param names A single name or an array of names to remove.
|
|
423
|
+
* @param {string| string[]} names A single name or an array of names to remove.
|
|
279
424
|
*/
|
|
280
425
|
remove(names) {
|
|
281
426
|
const namesToRemove = Array.isArray(names) ? names : [names];
|
|
282
|
-
const fieldsMap = new Map(this._fields.value);
|
|
283
427
|
const reallyRemoved = namesToRemove.filter((name) => {
|
|
284
|
-
const field =
|
|
428
|
+
const field = this._fields.get(name);
|
|
285
429
|
if (!field) {
|
|
286
430
|
return false;
|
|
287
431
|
}
|
|
288
432
|
field.destroy();
|
|
289
|
-
|
|
290
|
-
return true;
|
|
433
|
+
return this._fields.delete(name);
|
|
291
434
|
});
|
|
292
435
|
if (!reallyRemoved.length) {
|
|
293
436
|
return;
|
|
294
437
|
}
|
|
295
|
-
this.
|
|
296
|
-
this.events.emit("removed", { fieldNames: reallyRemoved });
|
|
438
|
+
this.onRemove.emit({ names: reallyRemoved });
|
|
297
439
|
}
|
|
298
440
|
/**
|
|
299
441
|
* Removes all fields from the collection, ensuring each is properly destroyed.
|
|
300
442
|
*/
|
|
301
443
|
clear() {
|
|
302
|
-
this.remove(Array.from(this._fields.
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Creates a serializable snapshot of the current state of all fields.
|
|
306
|
-
* @returns A plain JavaScript object representing the values of all fields.
|
|
307
|
-
*/
|
|
308
|
-
snapshot() {
|
|
309
|
-
const dump = {
|
|
310
|
-
__type: "Fields" /* fields */
|
|
311
|
-
};
|
|
312
|
-
this._fields.value.forEach((field, key) => dump[key] = field.val);
|
|
313
|
-
return dump;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Restores the state of the fields from a snapshot.
|
|
317
|
-
* It uses the `upset` logic to create or update fields based on the snapshot data.
|
|
318
|
-
* @param snapshot The snapshot object to load.
|
|
319
|
-
*/
|
|
320
|
-
hydrate(snapshot) {
|
|
321
|
-
for (let key in snapshot) {
|
|
322
|
-
if (key === "__type") {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
this.upset(key, snapshot[key]);
|
|
326
|
-
}
|
|
444
|
+
this.remove(Array.from(this._fields.keys()));
|
|
327
445
|
}
|
|
328
446
|
};
|
|
329
447
|
|
|
330
|
-
// src/fields.ts
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
upsetNumber(name, value, options) {
|
|
337
|
-
if (this.has(name)) {
|
|
338
|
-
const field = this.getNumber(name);
|
|
339
|
-
field.set(value);
|
|
340
|
-
return field;
|
|
448
|
+
// src/mixins/with-boolean-fields.mixin.ts
|
|
449
|
+
function WithBooleanFields(Base) {
|
|
450
|
+
return class FieldsWithBoolean extends Base {
|
|
451
|
+
createBoolean(name, initialValue, options) {
|
|
452
|
+
return this.create(DefaultBooleanField.typeName, name, initialValue, options);
|
|
341
453
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
getNumber(name) {
|
|
345
|
-
const field = this.get(name);
|
|
346
|
-
throwIf2(!(field instanceof NumberField), `wrong field type, field ${name} not a instance of NUmberField`);
|
|
347
|
-
return field;
|
|
348
|
-
}
|
|
349
|
-
create(name, initialValue) {
|
|
350
|
-
return this.add(new Field(name, initialValue));
|
|
351
|
-
}
|
|
352
|
-
upset(name, value) {
|
|
353
|
-
if (this.has(name)) {
|
|
354
|
-
const field = this.get(name);
|
|
355
|
-
field.set(value);
|
|
356
|
-
return field;
|
|
454
|
+
upsetBoolean(name, value, options) {
|
|
455
|
+
return this.upset(DefaultBooleanField.typeName, name, value, options);
|
|
357
456
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
};
|
|
457
|
+
getBoolean(name) {
|
|
458
|
+
return this.get(name);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
365
462
|
|
|
366
|
-
// src/
|
|
367
|
-
|
|
463
|
+
// src/mixins/with-string-fields.mixin.ts
|
|
464
|
+
function WithStringFields(Base) {
|
|
465
|
+
return class FieldsWithString extends Base {
|
|
466
|
+
createString(name, initialValue, options) {
|
|
467
|
+
return this.create(DefaultStringField.typeName, name, initialValue, options);
|
|
468
|
+
}
|
|
469
|
+
upsetString(name, value, options) {
|
|
470
|
+
return this.upset(DefaultStringField.typeName, name, value, options);
|
|
471
|
+
}
|
|
472
|
+
getString(name) {
|
|
473
|
+
return this.get(name);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/mixins/with-numeric-fields.mixin.ts
|
|
479
|
+
function WithNumericFields(Base) {
|
|
480
|
+
return class FieldsWithNumeric extends Base {
|
|
481
|
+
createNumeric(name, initialValue, options) {
|
|
482
|
+
return this.create(DefaultNumericField.typeName, name, initialValue, options);
|
|
483
|
+
}
|
|
484
|
+
upsetNumeric(name, value, options) {
|
|
485
|
+
return this.upset(DefaultNumericField.typeName, name, value, options);
|
|
486
|
+
}
|
|
487
|
+
getNumeric(name) {
|
|
488
|
+
return this.get(name);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/mixins/with-default-fields.mixin.ts
|
|
494
|
+
function WithDefaultFields(Base) {
|
|
495
|
+
return class FieldsWithDefault extends Base {
|
|
496
|
+
createDefault(name, initialValue, options) {
|
|
497
|
+
return this.create(DefaultField.typeName, name, initialValue, options);
|
|
498
|
+
}
|
|
499
|
+
upsetDefault(name, value, options) {
|
|
500
|
+
return this.upset(DefaultField.typeName, name, value, options);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/default-fields.ts
|
|
506
|
+
var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultFields(Fields)))) {
|
|
368
507
|
};
|
|
369
508
|
|
|
370
509
|
// src/field-tree.ts
|
|
371
|
-
import {
|
|
372
|
-
import { ensurePathArray, ensurePathString, throwIf as throwIf3, throwIfEmpty } from "@axi-engine/utils";
|
|
373
|
-
import { AxiEventEmitter as AxiEventEmitter2 } from "@axi-engine/events";
|
|
510
|
+
import { ensurePathArray, ensurePathString, throwIf as throwIf3, throwIfEmpty as throwIfEmpty2 } from "@axi-engine/utils";
|
|
374
511
|
var FieldTree = class _FieldTree {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
512
|
+
static typeName = "fieldTree";
|
|
513
|
+
typeName = _FieldTree.typeName;
|
|
514
|
+
/** @private The internal map storing child nodes (branches or leaves). */
|
|
515
|
+
_nodes = /* @__PURE__ */ new Map();
|
|
516
|
+
/** @private The factory used to create new child nodes. */
|
|
517
|
+
_factory;
|
|
518
|
+
/**
|
|
519
|
+
* Gets the collection of direct child nodes of this tree branch.
|
|
520
|
+
*/
|
|
521
|
+
get nodes() {
|
|
522
|
+
return this._nodes;
|
|
378
523
|
}
|
|
379
524
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
525
|
+
* Creates an instance of FieldTree.
|
|
526
|
+
* @param {TreeNodeFactory} factory - A factory responsible for creating new nodes within the tree.
|
|
382
527
|
*/
|
|
383
|
-
|
|
384
|
-
|
|
528
|
+
constructor(factory) {
|
|
529
|
+
this._factory = factory;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Checks if a direct child node with the given name exists.
|
|
533
|
+
* @param {string} name - The name of the direct child node.
|
|
534
|
+
* @returns {boolean} `true` if the node exists, otherwise `false`.
|
|
535
|
+
*/
|
|
536
|
+
has(name) {
|
|
537
|
+
return this._nodes.has(name);
|
|
385
538
|
}
|
|
386
539
|
/**
|
|
387
|
-
* Checks if a
|
|
388
|
-
* @
|
|
540
|
+
* Checks if a node exists at a given path, traversing the tree.
|
|
541
|
+
* @param {PathType} path - The path to check (e.g., 'player/stats' or ['player', 'stats']).
|
|
542
|
+
* @returns {boolean} `true` if the entire path resolves to a node, otherwise `false`.
|
|
389
543
|
*/
|
|
390
544
|
hasPath(path) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
for (let i = 0; i < pathParts.length; i++) {
|
|
394
|
-
const part = pathParts[i];
|
|
395
|
-
const nextNode = currentNode._items.value.get(part);
|
|
396
|
-
if (!nextNode) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
if (nextNode instanceof BaseFields) {
|
|
400
|
-
if (i === pathParts.length - 1) {
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
throwIf3(
|
|
404
|
-
pathParts.length - i > 2,
|
|
405
|
-
`Path validation failed, full path: ${ensurePathString(path)}, has extra nodes after Fields placed at: ${ensurePathString(pathParts.slice(0, i + 1))}`
|
|
406
|
-
);
|
|
407
|
-
return nextNode.has(pathParts[i + 1]);
|
|
408
|
-
}
|
|
409
|
-
currentNode = nextNode;
|
|
410
|
-
}
|
|
411
|
-
return true;
|
|
545
|
+
const traversedPath = this.traversePath(path);
|
|
546
|
+
return traversedPath.branch.has(traversedPath.leafName);
|
|
412
547
|
}
|
|
413
548
|
/**
|
|
414
|
-
*
|
|
415
|
-
* @param name The name
|
|
416
|
-
* @
|
|
417
|
-
* @
|
|
549
|
+
* Adds a pre-existing node as a direct child of this tree branch.
|
|
550
|
+
* @param {string} name - The name to assign to the new child node.
|
|
551
|
+
* @param {TreeOrFieldsNode} node - The node instance to add.
|
|
552
|
+
* @returns {TreeOrFieldsNode} The added node.
|
|
553
|
+
* @throws If a node with the same name already exists.
|
|
418
554
|
*/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
555
|
+
addNode(name, node) {
|
|
556
|
+
throwIf3(this.has(name), `Can't add node with name: '${name}', node already exists`);
|
|
557
|
+
this._nodes.set(name, node);
|
|
422
558
|
return node;
|
|
423
559
|
}
|
|
424
560
|
/**
|
|
425
|
-
* Retrieves a child node
|
|
426
|
-
* @param name The name of the child node.
|
|
427
|
-
* @returns The
|
|
428
|
-
* @throws If
|
|
561
|
+
* Retrieves a direct child node by its name.
|
|
562
|
+
* @param {string} name - The name of the child node.
|
|
563
|
+
* @returns {TreeOrFieldsNode} The retrieved node.
|
|
564
|
+
* @throws If a node with the given name cannot be found.
|
|
429
565
|
*/
|
|
430
|
-
|
|
431
|
-
const node = this.
|
|
432
|
-
|
|
566
|
+
getNode(name) {
|
|
567
|
+
const node = this._nodes.get(name);
|
|
568
|
+
throwIfEmpty2(node, `Can't find node with name '${name}'`);
|
|
433
569
|
return node;
|
|
434
570
|
}
|
|
435
571
|
/**
|
|
436
|
-
*
|
|
437
|
-
* @param
|
|
438
|
-
* @
|
|
439
|
-
* @
|
|
572
|
+
* Creates a new `FieldTree` (branch) node at the specified path.
|
|
573
|
+
* @param {PathType} path - The path where the new `FieldTree` should be created.
|
|
574
|
+
* @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
|
|
575
|
+
* @returns {FieldTree} The newly created `FieldTree` instance.
|
|
576
|
+
* @throws If the path is invalid or a node already exists at the target location.
|
|
577
|
+
*/
|
|
578
|
+
createFieldTree(path, createPath) {
|
|
579
|
+
const traversedPath = this.traversePath(path, createPath);
|
|
580
|
+
return traversedPath.branch.addNode(traversedPath.leafName, this._factory.tree());
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Creates a new `Fields` (leaf) container at the specified path.
|
|
584
|
+
* @param {PathType} path - The path where the new `Fields` container should be created.
|
|
585
|
+
* @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
|
|
586
|
+
* @returns {Fields} The newly created `Fields` instance.
|
|
587
|
+
* @throws If the path is invalid or a node already exists at the target location.
|
|
440
588
|
*/
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
589
|
+
createFields(path, createPath) {
|
|
590
|
+
const traversedPath = this.traversePath(path, createPath);
|
|
591
|
+
return traversedPath.branch.addNode(traversedPath.leafName, this._factory.fields());
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Retrieves a `FieldTree` (branch) node from a specified path.
|
|
595
|
+
* @param {PathType} path - The path to the `FieldTree` node.
|
|
596
|
+
* @returns {FieldTree} The `FieldTree` instance at the specified path.
|
|
597
|
+
* @throws If the path is invalid or the node at the path is not a `FieldTree`.
|
|
598
|
+
*/
|
|
599
|
+
getFieldTree(path) {
|
|
600
|
+
const traversedPath = this.traversePath(path);
|
|
601
|
+
const node = traversedPath.branch.getNode(traversedPath.leafName);
|
|
602
|
+
throwIf3(
|
|
603
|
+
!(node instanceof _FieldTree),
|
|
604
|
+
`Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of FieldTree`
|
|
605
|
+
);
|
|
444
606
|
return node;
|
|
445
607
|
}
|
|
446
608
|
/**
|
|
447
|
-
* Retrieves a
|
|
448
|
-
* @param
|
|
449
|
-
* @returns The
|
|
450
|
-
* @throws If
|
|
609
|
+
* Retrieves a `Fields` (leaf) container from a specified path.
|
|
610
|
+
* @param {PathType} path - The path to the `Fields` container.
|
|
611
|
+
* @returns {Fields} The `Fields` instance at the specified path.
|
|
612
|
+
* @throws If the path is invalid or the node at the path is not a `Fields` container.
|
|
451
613
|
*/
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
614
|
+
getFields(path) {
|
|
615
|
+
const traversedPath = this.traversePath(path);
|
|
616
|
+
const node = traversedPath.branch.getNode(traversedPath.leafName);
|
|
617
|
+
throwIf3(
|
|
618
|
+
!(node instanceof Fields),
|
|
619
|
+
`Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of Fields`
|
|
620
|
+
);
|
|
455
621
|
return node;
|
|
456
622
|
}
|
|
457
623
|
/**
|
|
458
|
-
*
|
|
459
|
-
* @param
|
|
460
|
-
* @returns The newly created `FieldTree` instance.
|
|
624
|
+
* Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
|
|
625
|
+
* @param {PathType} path - The path to the `FieldTree` node.
|
|
626
|
+
* @returns {FieldTree} The existing or newly created `FieldTree` instance.
|
|
627
|
+
*/
|
|
628
|
+
getOrCreateFieldTree(path) {
|
|
629
|
+
const traversedPath = this.traversePath(path, true);
|
|
630
|
+
return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFieldTree(traversedPath.leafName) : traversedPath.branch.createFieldTree(traversedPath.leafName);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
|
|
634
|
+
* @param {PathType} path - The path to the `Fields` container.
|
|
635
|
+
* @returns {Fields} The existing or newly created `Fields` instance.
|
|
461
636
|
*/
|
|
462
|
-
|
|
463
|
-
|
|
637
|
+
getOrCreateFields(path) {
|
|
638
|
+
const traversedPath = this.traversePath(path, true);
|
|
639
|
+
return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* @private
|
|
643
|
+
* Navigates the tree to the parent of a target node.
|
|
644
|
+
* This is the core traversal logic for all path-based operations.
|
|
645
|
+
* @param {PathType} path - The full path to the target node.
|
|
646
|
+
* @param {boolean} [createPath=false] - If `true`, creates missing `FieldTree` branches along the path.
|
|
647
|
+
* @returns {{branch: FieldTree, leafName: string}} An object containing the final branch (parent node) and the name of the leaf (target node).
|
|
648
|
+
* @throws If the path is empty, invalid, or contains a `Fields` container as an intermediate segment.
|
|
649
|
+
*/
|
|
650
|
+
traversePath(path, createPath) {
|
|
651
|
+
const pathArr = ensurePathArray(path);
|
|
652
|
+
throwIfEmpty2(pathArr, "The path is empty");
|
|
653
|
+
const leafName = pathArr.pop();
|
|
654
|
+
let currentNode = this;
|
|
655
|
+
for (const pathPart of pathArr) {
|
|
656
|
+
let node;
|
|
657
|
+
if (currentNode.has(pathPart)) {
|
|
658
|
+
node = currentNode.getNode(pathPart);
|
|
659
|
+
} else {
|
|
660
|
+
if (createPath) {
|
|
661
|
+
node = currentNode.createFieldTree(pathPart);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
throwIfEmpty2(node, `Can't find node with name ${pathPart} by path parsing: ${ensurePathString(path)}`);
|
|
665
|
+
throwIf3(node instanceof Fields, `Node with name ${pathPart} should be instance of FieldTree`);
|
|
666
|
+
currentNode = node;
|
|
667
|
+
}
|
|
668
|
+
return { branch: currentNode, leafName };
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/field-tree-node-factory.ts
|
|
673
|
+
var DefaultTreeNodeFactory = class {
|
|
674
|
+
constructor(fieldRegistry) {
|
|
675
|
+
this.fieldRegistry = fieldRegistry;
|
|
676
|
+
}
|
|
677
|
+
fields = () => new DefaultFields(this.fieldRegistry);
|
|
678
|
+
tree = () => new FieldTree(this);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/serializer/policies/clamp-policy-serializer-handler.ts
|
|
682
|
+
var ClampPolicySerializerHandler = class {
|
|
683
|
+
snapshot(policy) {
|
|
684
|
+
return { min: policy.min, max: policy.max };
|
|
685
|
+
}
|
|
686
|
+
hydrate(data) {
|
|
687
|
+
return new ClampPolicy(data.min, data.max);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/serializer/policies/clamp-max-policy-serializer-handler.ts
|
|
692
|
+
var ClampMaxPolicySerializerHandler = class {
|
|
693
|
+
snapshot(policy) {
|
|
694
|
+
return { max: policy.max };
|
|
695
|
+
}
|
|
696
|
+
hydrate(data) {
|
|
697
|
+
return new ClampMaxPolicy(data.max);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/serializer/policies/clamp-min-policy-serializer-handler.ts
|
|
702
|
+
var ClampMinPolicySerializerHandler = class {
|
|
703
|
+
snapshot(policy) {
|
|
704
|
+
return { min: policy.min };
|
|
705
|
+
}
|
|
706
|
+
hydrate(data) {
|
|
707
|
+
return new ClampMinPolicy(data.min);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/serializer/policy-serializer.ts
|
|
712
|
+
import { throwIf as throwIf4, throwIfEmpty as throwIfEmpty3 } from "@axi-engine/utils";
|
|
713
|
+
var PolicySerializer = class {
|
|
714
|
+
handlers = /* @__PURE__ */ new Map();
|
|
715
|
+
register(policyId, handler) {
|
|
716
|
+
throwIf4(this.handlers.has(policyId), `A handler for policy ID '${policyId}' is already registered.`);
|
|
717
|
+
this.handlers.set(policyId, handler);
|
|
718
|
+
return this;
|
|
719
|
+
}
|
|
720
|
+
clearHandlers() {
|
|
721
|
+
this.handlers.clear();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Creates a serializable snapshot of a policy instance.
|
|
725
|
+
* The snapshot includes the policy's state and a `__type` identifier.
|
|
726
|
+
* @param policy The policy instance to snapshot.
|
|
727
|
+
* @returns A plain object ready for JSON serialization.
|
|
728
|
+
* @throws If no handler is registered for the policy's ID.
|
|
729
|
+
*/
|
|
730
|
+
snapshot(policy) {
|
|
731
|
+
const handler = this.handlers.get(policy.id);
|
|
732
|
+
throwIfEmpty3(handler, `No serializer handler registered for policy ID: '${policy.id}'`);
|
|
733
|
+
const data = handler.snapshot(policy);
|
|
734
|
+
return {
|
|
735
|
+
__type: policy.id,
|
|
736
|
+
...data
|
|
737
|
+
};
|
|
464
738
|
}
|
|
465
739
|
/**
|
|
466
|
-
*
|
|
467
|
-
* @param
|
|
468
|
-
* @returns
|
|
740
|
+
* Restores a policy instance from its snapshot representation.
|
|
741
|
+
* @param snapshot The plain object snapshot, which must contain a `__type` property.
|
|
742
|
+
* @returns A new, fully functional policy instance.
|
|
743
|
+
* @throws If the snapshot is invalid or no handler is registered for its `__type`.
|
|
469
744
|
*/
|
|
470
|
-
|
|
471
|
-
|
|
745
|
+
hydrate(snapshot) {
|
|
746
|
+
const typeId = snapshot?.__type;
|
|
747
|
+
throwIfEmpty3(typeId, 'Invalid policy snapshot: missing "__type" identifier.');
|
|
748
|
+
const handler = this.handlers.get(typeId);
|
|
749
|
+
throwIfEmpty3(handler, `No serializer handler registered for policy ID: '${typeId}'`);
|
|
750
|
+
const { __type, ...data } = snapshot;
|
|
751
|
+
return handler.hydrate(data);
|
|
472
752
|
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/serializer/field-serializer.ts
|
|
756
|
+
import { isNullOrUndefined as isNullOrUndefined2, throwIfEmpty as throwIfEmpty4 } from "@axi-engine/utils";
|
|
757
|
+
var FieldSerializer = class {
|
|
473
758
|
/**
|
|
474
|
-
* Creates
|
|
475
|
-
* @param
|
|
476
|
-
* @
|
|
759
|
+
* Creates an instance of FieldSerializer.
|
|
760
|
+
* @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
|
|
761
|
+
* @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
|
|
477
762
|
*/
|
|
478
|
-
|
|
479
|
-
|
|
763
|
+
constructor(fieldRegistry, policySerializer) {
|
|
764
|
+
this.fieldRegistry = fieldRegistry;
|
|
765
|
+
this.policySerializer = policySerializer;
|
|
480
766
|
}
|
|
481
767
|
/**
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* @
|
|
485
|
-
* @
|
|
768
|
+
* Creates a serializable snapshot of a Field instance.
|
|
769
|
+
* The snapshot includes the field's type, name, current value, and the state of all its policies.
|
|
770
|
+
* @param {Field<any>} field - The Field instance to serialize.
|
|
771
|
+
* @returns {FieldSnapshot} A plain object ready for JSON serialization.
|
|
486
772
|
*/
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
773
|
+
snapshot(field) {
|
|
774
|
+
let snapshot = {
|
|
775
|
+
__type: field.typeName,
|
|
776
|
+
name: field.name,
|
|
777
|
+
value: field.value
|
|
778
|
+
};
|
|
779
|
+
if (!field.policies.isEmpty()) {
|
|
780
|
+
const serializedPolicies = [];
|
|
781
|
+
field.policies.items.forEach((policy) => serializedPolicies.push(this.policySerializer.snapshot(policy)));
|
|
782
|
+
snapshot.policies = serializedPolicies;
|
|
493
783
|
}
|
|
494
|
-
return
|
|
784
|
+
return snapshot;
|
|
495
785
|
}
|
|
496
786
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* @param
|
|
501
|
-
* @returns
|
|
787
|
+
* Restores a Field instance from its snapshot representation.
|
|
788
|
+
* It uses the `__type` property to find the correct constructor and hydrates
|
|
789
|
+
* the field with its value and all its policies.
|
|
790
|
+
* @param {FieldSnapshot} snapshot - The plain object snapshot to deserialize.
|
|
791
|
+
* @returns {Field<any>} A new, fully functional Field instance.
|
|
792
|
+
* @throws If the snapshot is invalid, missing a `__type`, or if the type is not registered.
|
|
502
793
|
*/
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
794
|
+
hydrate(snapshot) {
|
|
795
|
+
const fieldType = snapshot.__type;
|
|
796
|
+
throwIfEmpty4(fieldType, 'Invalid field snapshot: missing "__type" identifier.');
|
|
797
|
+
const Ctor = this.fieldRegistry.get(fieldType);
|
|
798
|
+
let policies;
|
|
799
|
+
if (!isNullOrUndefined2(snapshot.policies)) {
|
|
800
|
+
policies = [];
|
|
801
|
+
snapshot.policies.forEach((p) => policies.push(this.policySerializer.hydrate(p)));
|
|
802
|
+
}
|
|
803
|
+
return new Ctor(snapshot.name, snapshot.value, { policies });
|
|
508
804
|
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/serializer/fields-serializer.ts
|
|
808
|
+
var FieldsSerializer = class {
|
|
509
809
|
/**
|
|
510
|
-
* Creates
|
|
511
|
-
* @param
|
|
512
|
-
* @param
|
|
513
|
-
* @returns The newly created `NumberField` instance.
|
|
810
|
+
* Creates an instance of FieldsSerializer.
|
|
811
|
+
* @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
|
|
812
|
+
* @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
|
|
514
813
|
*/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
814
|
+
constructor(fieldRegistry, policySerializer) {
|
|
815
|
+
this.fieldRegistry = fieldRegistry;
|
|
816
|
+
this.policySerializer = policySerializer;
|
|
817
|
+
this.fieldSerializer = new FieldSerializer(this.fieldRegistry, this.policySerializer);
|
|
519
818
|
}
|
|
520
819
|
/**
|
|
521
|
-
*
|
|
522
|
-
* @
|
|
523
|
-
|
|
820
|
+
* An internal instance of FieldSerializer to handle individual fields.
|
|
821
|
+
* @private
|
|
822
|
+
*/
|
|
823
|
+
fieldSerializer;
|
|
824
|
+
/**
|
|
825
|
+
* Creates a serializable snapshot of a `Fields` container.
|
|
826
|
+
*
|
|
827
|
+
* The snapshot includes a `__type` identifier (currently hardcoded) and an array of snapshots
|
|
828
|
+
* for each `Field` within the container.
|
|
829
|
+
* @param {Fields} fields - The `Fields` instance to serialize.
|
|
830
|
+
* @returns {FieldsSnapshot} A plain object ready for JSON serialization.
|
|
524
831
|
*/
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
832
|
+
snapshot(fields) {
|
|
833
|
+
const res = {
|
|
834
|
+
__type: fields.typeName
|
|
835
|
+
};
|
|
836
|
+
fields.fields.forEach((field) => res[field.name] = this.fieldSerializer.snapshot(field));
|
|
837
|
+
return res;
|
|
529
838
|
}
|
|
530
839
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
840
|
+
* Restores a `Fields` container instance from its snapshot representation.
|
|
841
|
+
*
|
|
842
|
+
* **Limitation:** This method is currently hardcoded to always create an instance of `DefaultFields`.
|
|
843
|
+
* It iterates through the field snapshots and hydrates them individually, adding them to the new container.
|
|
844
|
+
* @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
|
|
845
|
+
* @returns {DefaultFields} A new `DefaultFields` instance populated with the restored fields.
|
|
534
846
|
*/
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
847
|
+
hydrate(snapshot) {
|
|
848
|
+
const { __type, ...fieldsData } = snapshot;
|
|
849
|
+
const fields = new DefaultFields(this.fieldRegistry);
|
|
850
|
+
for (const fieldName in fieldsData) {
|
|
851
|
+
const fieldSnapshot = fieldsData[fieldName];
|
|
852
|
+
const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
|
|
853
|
+
fields.add(restoredField);
|
|
854
|
+
}
|
|
855
|
+
return fields;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// src/serializer/field-tree-serializer.ts
|
|
860
|
+
import { isString } from "@axi-engine/utils";
|
|
861
|
+
var FieldTreeSerializer = class {
|
|
862
|
+
constructor(fieldTreeNodeFactory, fieldsSerializer) {
|
|
863
|
+
this.fieldTreeNodeFactory = fieldTreeNodeFactory;
|
|
864
|
+
this.fieldsSerializer = fieldsSerializer;
|
|
539
865
|
}
|
|
540
866
|
/**
|
|
541
867
|
* Creates a serializable snapshot of the entire tree and its contained fields.
|
|
542
868
|
* @returns A plain JavaScript object representing the complete state managed by this tree.
|
|
543
869
|
*/
|
|
544
|
-
snapshot() {
|
|
545
|
-
const
|
|
546
|
-
__type:
|
|
870
|
+
snapshot(tree) {
|
|
871
|
+
const res = {
|
|
872
|
+
__type: tree.typeName
|
|
547
873
|
};
|
|
548
|
-
|
|
549
|
-
|
|
874
|
+
tree.nodes.forEach((node, key) => {
|
|
875
|
+
if (node.typeName === tree.typeName) {
|
|
876
|
+
res[key] = this.snapshot(node);
|
|
877
|
+
} else if (node.typeName === Fields.typeName) {
|
|
878
|
+
res[key] = this.fieldsSerializer.snapshot(node);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return res;
|
|
550
882
|
}
|
|
551
883
|
/**
|
|
552
884
|
* Restores the state of the tree from a snapshot.
|
|
@@ -554,60 +886,43 @@ var FieldTree = class _FieldTree {
|
|
|
554
886
|
* @param snapshot The snapshot object to load.
|
|
555
887
|
*/
|
|
556
888
|
hydrate(snapshot) {
|
|
557
|
-
|
|
558
|
-
|
|
889
|
+
const { __type, ...nodes } = snapshot;
|
|
890
|
+
const tree = this.fieldTreeNodeFactory.tree();
|
|
891
|
+
for (const key in nodes) {
|
|
892
|
+
const nodeData = nodes[key];
|
|
893
|
+
if (isString(nodeData)) {
|
|
559
894
|
continue;
|
|
560
895
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (type === "Fields" /* fields */) {
|
|
566
|
-
node = this.createFields(key);
|
|
567
|
-
} else if (type === "FieldTree" /* fieldTree */) {
|
|
568
|
-
node = this.createFieldTree(key);
|
|
569
|
-
} else {
|
|
570
|
-
console.warn(`Node '${key}' in snapshot has no __type metadata. Skipping.`);
|
|
571
|
-
}
|
|
896
|
+
if (nodeData.__type === FieldTree.typeName) {
|
|
897
|
+
tree.addNode(key, this.hydrate(nodeData));
|
|
898
|
+
} else {
|
|
899
|
+
tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
|
|
572
900
|
}
|
|
573
|
-
node?.hydrate(field);
|
|
574
901
|
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* @private
|
|
578
|
-
* Generic internal method for creating and adding a new node to the tree.
|
|
579
|
-
* @param name The name of the node to create.
|
|
580
|
-
* @param ctor The constructor for the node type (e.g., `FieldTree` or `Fields`).
|
|
581
|
-
* @returns The newly created node instance.
|
|
582
|
-
*/
|
|
583
|
-
createNode(name, ctor) {
|
|
584
|
-
const currentItems = this._items.value;
|
|
585
|
-
throwIf3(currentItems.has(name), `Can't create node with name: '${name}', node already exists`);
|
|
586
|
-
const res = new ctor();
|
|
587
|
-
const newItems = new Map(currentItems);
|
|
588
|
-
newItems.set(name, res);
|
|
589
|
-
this._items.value = newItems;
|
|
590
|
-
this.events.emit("created", {
|
|
591
|
-
type: "created",
|
|
592
|
-
name,
|
|
593
|
-
path: [],
|
|
594
|
-
// todo: need to decide how to pass full path
|
|
595
|
-
node: res
|
|
596
|
-
});
|
|
597
|
-
return res;
|
|
902
|
+
return tree;
|
|
598
903
|
}
|
|
599
904
|
};
|
|
600
905
|
export {
|
|
601
|
-
BaseFields,
|
|
602
906
|
ClampMaxPolicy,
|
|
907
|
+
ClampMaxPolicySerializerHandler,
|
|
603
908
|
ClampMinPolicy,
|
|
909
|
+
ClampMinPolicySerializerHandler,
|
|
604
910
|
ClampPolicy,
|
|
605
|
-
|
|
911
|
+
ClampPolicySerializerHandler,
|
|
912
|
+
DefaultBooleanField,
|
|
913
|
+
DefaultField,
|
|
914
|
+
DefaultFields,
|
|
915
|
+
DefaultNumericField,
|
|
916
|
+
DefaultStringField,
|
|
917
|
+
DefaultTreeNodeFactory,
|
|
918
|
+
FieldRegistry,
|
|
919
|
+
FieldSerializer,
|
|
606
920
|
FieldTree,
|
|
921
|
+
FieldTreeSerializer,
|
|
607
922
|
Fields,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
923
|
+
FieldsSerializer,
|
|
924
|
+
Policies,
|
|
925
|
+
PolicySerializer,
|
|
611
926
|
clampMaxPolicy,
|
|
612
927
|
clampMinPolicy,
|
|
613
928
|
clampPolicy
|