@axi-engine/fields 0.3.1 → 0.3.3

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/index.mjs CHANGED
@@ -1,1214 +1,1202 @@
1
- // src/policies/clamp-policy.ts
2
- var ClampPolicy = class _ClampPolicy {
3
- constructor(min, max) {
4
- this.min = min;
5
- this.max = max;
6
- }
7
- static id = "clamp";
8
- id = _ClampPolicy.id;
9
- apply(val) {
10
- return Math.max(this.min, Math.min(this.max, val));
11
- }
12
- updateBounds(min, max) {
13
- this.min = min;
14
- this.max = max;
15
- }
1
+ import { ConstructorRegistry, Emitter, ensurePathArray, ensurePathString, isBoolean, isNullOrUndefined, isNumber, isString, throwIf, throwIfEmpty } from "@axi-engine/utils";
2
+ import { dequal } from "dequal";
3
+
4
+ //#region src/policies/clamp-policy.ts
5
+ var ClampPolicy = class ClampPolicy {
6
+ static id = "clamp";
7
+ id = ClampPolicy.id;
8
+ constructor(min, max) {
9
+ this.min = min;
10
+ this.max = max;
11
+ }
12
+ apply(val) {
13
+ return Math.max(this.min, Math.min(this.max, val));
14
+ }
15
+ updateBounds(min, max) {
16
+ this.min = min;
17
+ this.max = max;
18
+ }
16
19
  };
17
20
  function clampPolicy(min, max) {
18
- return new ClampPolicy(min, max);
21
+ return new ClampPolicy(min, max);
19
22
  }
20
23
 
21
- // src/policies/clamp-max-policy.ts
22
- var ClampMaxPolicy = class _ClampMaxPolicy {
23
- constructor(max) {
24
- this.max = max;
25
- }
26
- static id = "clampMax";
27
- id = _ClampMaxPolicy.id;
28
- apply(val) {
29
- return Math.min(this.max, val);
30
- }
31
- updateBounds(max) {
32
- this.max = max;
33
- }
24
+ //#endregion
25
+ //#region src/policies/clamp-max-policy.ts
26
+ var ClampMaxPolicy = class ClampMaxPolicy {
27
+ static id = "clampMax";
28
+ id = ClampMaxPolicy.id;
29
+ constructor(max) {
30
+ this.max = max;
31
+ }
32
+ apply(val) {
33
+ return Math.min(this.max, val);
34
+ }
35
+ updateBounds(max) {
36
+ this.max = max;
37
+ }
34
38
  };
35
39
  function clampMaxPolicy(max) {
36
- return new ClampMaxPolicy(max);
40
+ return new ClampMaxPolicy(max);
37
41
  }
38
42
 
39
- // src/policies/clamp-min-policy.ts
40
- var ClampMinPolicy = class _ClampMinPolicy {
41
- constructor(min) {
42
- this.min = min;
43
- }
44
- static id = "clampMin";
45
- id = _ClampMinPolicy.id;
46
- apply(val) {
47
- return Math.max(this.min, val);
48
- }
49
- updateBounds(min) {
50
- this.min = min;
51
- }
43
+ //#endregion
44
+ //#region src/policies/clamp-min-policy.ts
45
+ var ClampMinPolicy = class ClampMinPolicy {
46
+ static id = "clampMin";
47
+ id = ClampMinPolicy.id;
48
+ constructor(min) {
49
+ this.min = min;
50
+ }
51
+ apply(val) {
52
+ return Math.max(this.min, val);
53
+ }
54
+ updateBounds(min) {
55
+ this.min = min;
56
+ }
52
57
  };
53
58
  function clampMinPolicy(min) {
54
- return new ClampMinPolicy(min);
59
+ return new ClampMinPolicy(min);
55
60
  }
56
61
 
57
- // src/policies/policies.ts
62
+ //#endregion
63
+ //#region src/policies/policies.ts
58
64
  var Policies = class {
59
- policies = /* @__PURE__ */ new Map();
60
- get items() {
61
- return this.policies;
62
- }
63
- /**
64
- * Retrieves a specific policy instance by its ID.
65
- * Useful for accessing a policy's internal state or methods.
66
- * @template P The expected type of the policy.
67
- * @param id The unique ID of the policy to retrieve.
68
- * @returns The policy instance, or `undefined` if not found.
69
- */
70
- get(id) {
71
- return this.policies.get(id);
72
- }
73
- /**
74
- * Adds a new policy to the field or replaces an existing one with the same ID.
75
- * The new policy will be applied on the next `set()` operation.
76
- * If a policy with the same ID already exists, its `destroy` method will be called before it is replaced.
77
- * @param policy The policy instance to add.
78
- */
79
- add(policy) {
80
- const existed = this.policies.get(policy.id);
81
- existed?.destroy?.();
82
- this.policies.set(policy.id, policy);
83
- return this;
84
- }
85
- /**
86
- * Removes a policy from the field by its ID and call `destroy` method.
87
- * @param policyId The unique ID of the policy to remove.
88
- * @returns `true` if the policy was found and removed, otherwise `false`.
89
- */
90
- remove(policyId) {
91
- const policyToRemove = this.policies.get(policyId);
92
- if (!policyToRemove) {
93
- return false;
94
- }
95
- policyToRemove.destroy?.();
96
- return this.policies.delete(policyId);
97
- }
98
- isEmpty() {
99
- return this.policies.size === 0;
100
- }
101
- /**
102
- * Removes all policies from the field.
103
- * After this, `set()` will no longer apply any transformations to the value until new policies are added.
104
- */
105
- clear() {
106
- this.policies.forEach((policy) => policy.destroy?.());
107
- this.policies.clear();
108
- }
109
- /**
110
- * Forces the current value to be re-processed by all policies.
111
- * Useful if a policy's logic has changed and you need to re-evaluate the current state.
112
- */
113
- apply(val) {
114
- let finalVal = val;
115
- this.policies.forEach((policy) => finalVal = policy.apply(finalVal));
116
- return finalVal;
117
- }
65
+ policies = /* @__PURE__ */ new Map();
66
+ get items() {
67
+ return this.policies;
68
+ }
69
+ /**
70
+ * Retrieves a specific policy instance by its ID.
71
+ * Useful for accessing a policy's internal state or methods.
72
+ * @template P The expected type of the policy.
73
+ * @param id The unique ID of the policy to retrieve.
74
+ * @returns The policy instance, or `undefined` if not found.
75
+ */
76
+ get(id) {
77
+ return this.policies.get(id);
78
+ }
79
+ /**
80
+ * Adds a new policy to the field or replaces an existing one with the same ID.
81
+ * The new policy will be applied on the next `set()` operation.
82
+ * If a policy with the same ID already exists, its `destroy` method will be called before it is replaced.
83
+ * @param policy The policy instance to add.
84
+ */
85
+ add(policy) {
86
+ this.policies.get(policy.id)?.destroy?.();
87
+ this.policies.set(policy.id, policy);
88
+ return this;
89
+ }
90
+ /**
91
+ * Removes a policy from the field by its ID and call `destroy` method.
92
+ * @param policyId The unique ID of the policy to remove.
93
+ * @returns `true` if the policy was found and removed, otherwise `false`.
94
+ */
95
+ remove(policyId) {
96
+ const policyToRemove = this.policies.get(policyId);
97
+ if (!policyToRemove) return false;
98
+ policyToRemove.destroy?.();
99
+ return this.policies.delete(policyId);
100
+ }
101
+ isEmpty() {
102
+ return this.policies.size === 0;
103
+ }
104
+ /**
105
+ * Removes all policies from the field.
106
+ * After this, `set()` will no longer apply any transformations to the value until new policies are added.
107
+ */
108
+ clear() {
109
+ this.policies.forEach((policy) => policy.destroy?.());
110
+ this.policies.clear();
111
+ }
112
+ /**
113
+ * Forces the current value to be re-processed by all policies.
114
+ * Useful if a policy's logic has changed and you need to re-evaluate the current state.
115
+ */
116
+ apply(val) {
117
+ let finalVal = val;
118
+ this.policies.forEach((policy) => finalVal = policy.apply(finalVal));
119
+ return finalVal;
120
+ }
118
121
  };
119
122
 
120
- // src/mixins/mixin-factory.ts
123
+ //#endregion
124
+ //#region src/mixins/mixin-factory.ts
125
+ /**
126
+ * A higher-order function that generates a mixin for a specific Field type.
127
+ * This factory removes the need to write boilerplate mixin code for every new field type.
128
+ *
129
+ * @param typeName The `typeName` of the Field to create (e.g., 'boolean', 'my-signal-field').
130
+ * @param baseMethodName The base name for the generated methods (e.g., 'Boolean', 'MySignal').
131
+ * @returns A fully functional, typed mixin.
132
+ */
121
133
  function createTypedMethodsMixin(typeName, baseMethodName) {
122
- const methodNames = {
123
- create: `create${baseMethodName}`,
124
- upset: `upset${baseMethodName}`,
125
- get: `get${baseMethodName}`
126
- };
127
- return function(Base) {
128
- return class FieldsWith extends Base {
129
- // createBoolean, createMySignal, etc.
130
- [methodNames.create](name, initialValue, options) {
131
- return this.create(typeName, name, initialValue, options);
132
- }
133
- // upsetBoolean, upsetMySignal, etc.
134
- [methodNames.upset](name, value, options) {
135
- return this.upset(typeName, name, value, options);
136
- }
137
- // getBoolean, getMySignal, etc.
138
- [methodNames.get](name) {
139
- return this.get(name);
140
- }
141
- };
142
- };
134
+ const methodNames = {
135
+ create: `create${baseMethodName}`,
136
+ upset: `upset${baseMethodName}`,
137
+ get: `get${baseMethodName}`
138
+ };
139
+ return function(Base) {
140
+ return class FieldsWith extends Base {
141
+ [methodNames.create](name, initialValue, options) {
142
+ return this.create(typeName, name, initialValue, options);
143
+ }
144
+ [methodNames.upset](name, value, options) {
145
+ return this.upset(typeName, name, value, options);
146
+ }
147
+ [methodNames.get](name) {
148
+ return this.get(name);
149
+ }
150
+ };
151
+ };
143
152
  }
144
153
 
145
- // src/field-definitions/core-field.ts
146
- import { Emitter } from "@axi-engine/utils";
147
- import { dequal } from "dequal";
148
- var CoreField = class _CoreField {
149
- /** A type keyword of the field */
150
- static typeName = "default";
151
- typeName = _CoreField.typeName;
152
- /** A unique identifier for the field. */
153
- _name;
154
- _value;
155
- _onChange = new Emitter();
156
- onChange;
157
- policies = new Policies();
158
- get name() {
159
- return this._name;
160
- }
161
- /**
162
- * Gets the current raw value of the field.
163
- * For reactive updates, it's recommended to use the `.signal` property instead.
164
- */
165
- get value() {
166
- return this._value;
167
- }
168
- /**
169
- * Sets a new value for the field.
170
- * The provided value will be processed by all registered policies before the underlying signal is updated.
171
- * @param val The new value to set.
172
- */
173
- set value(val) {
174
- const oldVal = this._value;
175
- const finalVal = this.policies.apply(val);
176
- if (!dequal(this._value, finalVal)) {
177
- this._value = finalVal;
178
- this._onChange.emit(this._value, oldVal);
179
- }
180
- }
181
- /**
182
- * Creates an instance of a Field.
183
- * @param name A unique identifier for the field.
184
- * @param initialVal The initial value of the field.
185
- * @param options Optional configuration for the field.
186
- * @param options.policies An array of policies to apply to the field's value on every `set` operation.
187
- * @param options.isEqual An function for compare old and new value, by default uses the strictEquals from `utils`
188
- *
189
- */
190
- constructor(name, initialVal, options) {
191
- this.onChange = this._onChange;
192
- this._name = name;
193
- options?.policies?.forEach((policy) => this.policies.add(policy));
194
- this.value = initialVal;
195
- }
196
- setValueSilently(val) {
197
- this._value = this.policies.apply(val);
198
- }
199
- batchUpdate(updateFn) {
200
- this.value = updateFn(this.value);
201
- }
202
- /**
203
- * Cleans up resources used by the field and its policies.
204
- * This should be called when the field is no longer needed to prevent memory leaks from reactive policies.
205
- */
206
- destroy() {
207
- this.policies.clear();
208
- this._onChange.clear();
209
- }
210
- };
211
-
212
- // src/field-definitions/core-boolean-field.ts
213
- var CoreBooleanField = class _CoreBooleanField extends CoreField {
214
- static typeName = "boolean";
215
- typeName = _CoreBooleanField.typeName;
216
- constructor(name, initialVal, options) {
217
- super(name, initialVal, options);
218
- }
219
- toggle() {
220
- this.value = !this.value;
221
- return this.value;
222
- }
154
+ //#endregion
155
+ //#region src/field-definitions/core-field.ts
156
+ /**
157
+ * A state container that wraps a value.
158
+ * It allows applying a pipeline of transformation or validation "policies" before any new value is set.
159
+ *
160
+ * @template T The type of the value this field holds.
161
+ *
162
+ */
163
+ var CoreField = class CoreField {
164
+ /** A type keyword of the field */
165
+ static typeName = "default";
166
+ typeName = CoreField.typeName;
167
+ /** A unique identifier for the field. */
168
+ _name;
169
+ _value;
170
+ _onChange = new Emitter();
171
+ onChange;
172
+ policies = new Policies();
173
+ get name() {
174
+ return this._name;
175
+ }
176
+ /**
177
+ * Gets the current raw value of the field.
178
+ * For reactive updates, it's recommended to use the `.signal` property instead.
179
+ */
180
+ get value() {
181
+ return this._value;
182
+ }
183
+ /**
184
+ * Sets a new value for the field.
185
+ * The provided value will be processed by all registered policies before the underlying signal is updated.
186
+ * @param val The new value to set.
187
+ */
188
+ set value(val) {
189
+ const oldVal = this._value;
190
+ const finalVal = this.policies.apply(val);
191
+ if (!dequal(this._value, finalVal)) {
192
+ this._value = finalVal;
193
+ this._onChange.emit(this._value, oldVal);
194
+ }
195
+ }
196
+ /**
197
+ * Creates an instance of a Field.
198
+ * @param name A unique identifier for the field.
199
+ * @param initialVal The initial value of the field.
200
+ * @param options Optional configuration for the field.
201
+ * @param options.policies An array of policies to apply to the field's value on every `set` operation.
202
+ * @param options.isEqual An function for compare old and new value, by default uses the strictEquals from `utils`
203
+ *
204
+ */
205
+ constructor(name, initialVal, options) {
206
+ this.onChange = this._onChange;
207
+ this._name = name;
208
+ options?.policies?.forEach((policy) => this.policies.add(policy));
209
+ this.value = initialVal;
210
+ }
211
+ setValueSilently(val) {
212
+ this._value = this.policies.apply(val);
213
+ }
214
+ batchUpdate(updateFn) {
215
+ this.value = updateFn(this.value);
216
+ }
217
+ /**
218
+ * Cleans up resources used by the field and its policies.
219
+ * This should be called when the field is no longer needed to prevent memory leaks from reactive policies.
220
+ */
221
+ destroy() {
222
+ this.policies.clear();
223
+ this._onChange.clear();
224
+ }
223
225
  };
224
226
 
225
- // src/field-definitions/core-string-field.ts
226
- var CoreStringField = class _CoreStringField extends CoreField {
227
- static typeName = "string";
228
- typeName = _CoreStringField.typeName;
229
- constructor(name, initialVal, options) {
230
- super(name, initialVal, options);
231
- }
232
- append(str) {
233
- this.value = this.value + str;
234
- return this;
235
- }
236
- prepend(str) {
237
- this.value = str + this.value;
238
- return this;
239
- }
240
- trim() {
241
- this.value = this.value.trim();
242
- return this;
243
- }
244
- isEmpty() {
245
- return this.value.length === 0;
246
- }
247
- clear() {
248
- this.value = "";
249
- }
227
+ //#endregion
228
+ //#region src/field-definitions/core-boolean-field.ts
229
+ var CoreBooleanField = class CoreBooleanField extends CoreField {
230
+ static typeName = "boolean";
231
+ typeName = CoreBooleanField.typeName;
232
+ constructor(name, initialVal, options) {
233
+ super(name, initialVal, options);
234
+ }
235
+ toggle() {
236
+ this.value = !this.value;
237
+ return this.value;
238
+ }
250
239
  };
251
240
 
252
- // src/field-definitions/core-numeric-field.ts
253
- import { isNullOrUndefined } from "@axi-engine/utils";
254
- var CoreNumericField = class _CoreNumericField extends CoreField {
255
- static typeName = "numeric";
256
- typeName = _CoreNumericField.typeName;
257
- get min() {
258
- const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMinPolicy.id);
259
- return policy?.min;
260
- }
261
- get max() {
262
- const policy = this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMaxPolicy.id);
263
- return policy?.max;
264
- }
265
- constructor(name, initialVal, options) {
266
- const policies = options?.policies ?? [];
267
- if (!isNullOrUndefined(options?.min) && !isNullOrUndefined(options?.max)) {
268
- policies.unshift(clampPolicy(options.min, options.max));
269
- } else if (!isNullOrUndefined(options?.min)) {
270
- policies.unshift(clampMinPolicy(options.min));
271
- } else if (!isNullOrUndefined(options?.max)) {
272
- policies.unshift(clampMaxPolicy(options.max));
273
- }
274
- super(name, initialVal, { policies });
275
- }
276
- isMin() {
277
- const min = this.min;
278
- return isNullOrUndefined(min) ? false : this.value <= min;
279
- }
280
- isMax() {
281
- const max = this.max;
282
- return isNullOrUndefined(max) ? false : this.value >= max;
283
- }
284
- inc(amount = 1) {
285
- this.value = this.value + amount;
286
- }
287
- dec(amount = 1) {
288
- this.value = this.value - amount;
289
- }
241
+ //#endregion
242
+ //#region src/field-definitions/core-string-field.ts
243
+ var CoreStringField = class CoreStringField extends CoreField {
244
+ static typeName = "string";
245
+ typeName = CoreStringField.typeName;
246
+ constructor(name, initialVal, options) {
247
+ super(name, initialVal, options);
248
+ }
249
+ append(str) {
250
+ this.value = this.value + str;
251
+ return this;
252
+ }
253
+ prepend(str) {
254
+ this.value = str + this.value;
255
+ return this;
256
+ }
257
+ trim() {
258
+ this.value = this.value.trim();
259
+ return this;
260
+ }
261
+ isEmpty() {
262
+ return this.value.length === 0;
263
+ }
264
+ clear() {
265
+ this.value = "";
266
+ }
290
267
  };
291
268
 
292
- // src/utils/constructor-registry.ts
293
- import { throwIf, throwIfEmpty } from "@axi-engine/utils";
294
- var ConstructorRegistry = class {
295
- items = /* @__PURE__ */ new Map();
296
- /**
297
- * Registers a constructor with a unique string identifier.
298
- *
299
- * @param typeId - The unique identifier for the constructor (e.g., a static `typeName` property from a class).
300
- * @param ctor - The class constructor to register.
301
- * @returns The registry instance for chainable calls.
302
- * @throws If a constructor with the same `typeId` is already registered.
303
- */
304
- register(typeId, ctor) {
305
- throwIf(this.items.has(typeId), `A constructor with typeId '${typeId}' is already registered.`);
306
- this.items.set(typeId, ctor);
307
- return this;
308
- }
309
- /**
310
- * Retrieves a constructor by its identifier.
311
- *
312
- * @param typeId - The identifier of the constructor to retrieve.
313
- * @returns The found class constructor.
314
- * @throws If no constructor is found for the given `typeId`.
315
- */
316
- get(typeId) {
317
- const Ctor = this.items.get(typeId);
318
- throwIfEmpty(Ctor, `No constructor found for typeId '${typeId}'`);
319
- return Ctor;
320
- }
321
- /**
322
- * Checks if a constructor for a given identifier is registered.
323
- * @param typeId - The identifier to check.
324
- * @returns `true` if a constructor is registered, otherwise `false`.
325
- */
326
- has(typeId) {
327
- return this.items.has(typeId);
328
- }
329
- /**
330
- * Clears all registered constructors from the registry.
331
- */
332
- clear() {
333
- this.items.clear();
334
- }
269
+ //#endregion
270
+ //#region src/field-definitions/core-numeric-field.ts
271
+ var CoreNumericField = class CoreNumericField extends CoreField {
272
+ static typeName = "numeric";
273
+ typeName = CoreNumericField.typeName;
274
+ get min() {
275
+ return (this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMinPolicy.id))?.min;
276
+ }
277
+ get max() {
278
+ return (this.policies.get(ClampPolicy.id) ?? this.policies.get(ClampMaxPolicy.id))?.max;
279
+ }
280
+ constructor(name, initialVal, options) {
281
+ const policies = options?.policies ?? [];
282
+ if (!isNullOrUndefined(options?.min) && !isNullOrUndefined(options?.max)) policies.unshift(clampPolicy(options.min, options.max));
283
+ else if (!isNullOrUndefined(options?.min)) policies.unshift(clampMinPolicy(options.min));
284
+ else if (!isNullOrUndefined(options?.max)) policies.unshift(clampMaxPolicy(options.max));
285
+ super(name, initialVal, { policies });
286
+ }
287
+ isMin() {
288
+ const min = this.min;
289
+ return isNullOrUndefined(min) ? false : this.value <= min;
290
+ }
291
+ isMax() {
292
+ const max = this.max;
293
+ return isNullOrUndefined(max) ? false : this.value >= max;
294
+ }
295
+ inc(amount = 1) {
296
+ this.value = this.value + amount;
297
+ }
298
+ dec(amount = 1) {
299
+ this.value = this.value - amount;
300
+ }
335
301
  };
336
302
 
337
- // src/field-registry.ts
338
- var FieldRegistry = class extends ConstructorRegistry {
339
- };
303
+ //#endregion
304
+ //#region src/field-registry.ts
305
+ var FieldRegistry = class extends ConstructorRegistry {};
340
306
 
341
- // src/fields.ts
342
- import { Emitter as Emitter2, throwIf as throwIf2 } from "@axi-engine/utils";
343
- var Fields = class _Fields {
344
- static typeName = "fields";
345
- typeName = _Fields.typeName;
346
- _fields = /* @__PURE__ */ new Map();
347
- _fieldRegistry;
348
- /**
349
- * An event emitter that fires when a new field is added to the collection.
350
- * @event
351
- * @param {object} event - The event payload.
352
- * @param {string} event.name - The name of the added field.
353
- * @param {Field<any>} event.field - The `Field` instance that was added.
354
- */
355
- onAdd = new Emitter2();
356
- /**
357
- * An event emitter that fires after one or more fields have been removed.
358
- * @event
359
- * @param {object} event - The event payload.
360
- * @param {string[]} event.names - An array of names of the fields that were successfully removed.
361
- */
362
- onRemove = new Emitter2();
363
- /**
364
- * Gets the read-only map of all `Field` instances in this container.
365
- * @returns {Map<string, Field<any>>} The collection of fields.
366
- */
367
- get fields() {
368
- return this._fields;
369
- }
370
- /**
371
- * Creates an instance of Fields.
372
- * @param {FieldRegistry} fieldRegistry - The registry used to create new `Field` instances.
373
- */
374
- constructor(fieldRegistry) {
375
- this._fieldRegistry = fieldRegistry;
376
- }
377
- /**
378
- * Checks if a field with the given name exists in the collection.
379
- * @param {string} name The name of the field to check.
380
- * @returns {boolean} `true` if the field exists, otherwise `false`.
381
- */
382
- has(name) {
383
- return this._fields.has(name);
384
- }
385
- /**
386
- * Adds a pre-existing `Field` instance to the collection and fires the `onAdd` event.
387
- * @template T - The specific `Field` type being added.
388
- * @param {Field<any>} field - The `Field` instance to add.
389
- * @returns {T} The added `Field` instance, cast to type `T`.
390
- * @throws If a field with the same name already exists.
391
- */
392
- add(field) {
393
- throwIf2(this.has(field.name), `Field with name '${field.name}' already exists`);
394
- this._fields.set(field.name, field);
395
- this.onAdd.emit({
396
- name: field.name,
397
- field
398
- });
399
- return field;
400
- }
401
- /**
402
- * Creates a new `Field` instance of a specified type, adds it to the collection, and returns it.
403
- * This is the primary factory method for creating fields within this container.
404
- * @template T - The expected `Field` type to be returned.
405
- * @param {string} typeName - The registered type name of the field to create (e.g., 'numeric', 'boolean').
406
- * @param {string} name - The unique name for the new field.
407
- * @param {*} initialValue - The initial value for the new field.
408
- * @param {*} [options] - Optional configuration passed to the field's constructor.
409
- * @returns {T} The newly created `Field` instance.
410
- */
411
- create(typeName, name, initialValue, options) {
412
- const Ctor = this._fieldRegistry.get(typeName);
413
- const field = new Ctor(name, initialValue, options);
414
- this.add(field);
415
- return field;
416
- }
417
- /**
418
- * Updates an existing field's value or creates a new one if it doesn't exist.
419
- * @template T - The expected `Field` type.
420
- * @param {string} typeName - The type name to use if a new field needs to be created.
421
- * @param {string} name - The name of the field to update or create.
422
- * @param {*} value - The new value to set.
423
- * @param {*} [options] - Optional configuration, used only if a new field is created.
424
- * @returns {T} The existing or newly created `Field` instance.
425
- */
426
- upset(typeName, name, value, options) {
427
- if (this.has(name)) {
428
- const field = this.get(name);
429
- field.value = value;
430
- return field;
431
- }
432
- return this.create(typeName, name, value, options);
433
- }
434
- /**
435
- * Retrieves a field by its name.
436
- * @template TField - The expected `Field` type to be returned.
437
- * @param {string} name - The name of the field to retrieve.
438
- * @returns {TField} The `Field` instance.
439
- * @throws If the field does not exist.
440
- */
441
- get(name) {
442
- throwIf2(!this._fields.has(name), `Field with name '${name}' not exists`);
443
- return this._fields.get(name);
444
- }
445
- /**
446
- * Removes one or more fields from the collection.
447
- * This method ensures that the `destroy` method of each removed field is called to clean up its resources.
448
- * @param {string| string[]} names A single name or an array of names to remove.
449
- */
450
- remove(names) {
451
- const namesToRemove = Array.isArray(names) ? names : [names];
452
- const reallyRemoved = namesToRemove.filter((name) => {
453
- const field = this._fields.get(name);
454
- if (!field) {
455
- return false;
456
- }
457
- field.destroy();
458
- return this._fields.delete(name);
459
- });
460
- if (!reallyRemoved.length) {
461
- return;
462
- }
463
- this.onRemove.emit({ names: reallyRemoved });
464
- }
465
- /**
466
- * Removes all fields from the collection, ensuring each is properly destroyed.
467
- */
468
- clear() {
469
- this.remove(Array.from(this._fields.keys()));
470
- }
471
- destroy() {
472
- this.clear();
473
- this.onAdd.clear();
474
- this.onRemove.clear();
475
- }
307
+ //#endregion
308
+ //#region src/fields.ts
309
+ /**
310
+ * A container for a collection of named `Field` instances.
311
+ *
312
+ * This class acts as a "leaf" node in the `FieldTree` hierarchy, managing a flat
313
+ * key-value store of reactive data points. It uses a `FieldRegistry` to dynamically
314
+ * create `Field` instances of different types.
315
+ */
316
+ var Fields = class Fields {
317
+ static typeName = "fields";
318
+ typeName = Fields.typeName;
319
+ _fields = /* @__PURE__ */ new Map();
320
+ _fieldRegistry;
321
+ /**
322
+ * An event emitter that fires when a new field is added to the collection.
323
+ * @event
324
+ * @param {object} event - The event payload.
325
+ * @param {string} event.name - The name of the added field.
326
+ * @param {Field<any>} event.field - The `Field` instance that was added.
327
+ */
328
+ onAdd = new Emitter();
329
+ /**
330
+ * An event emitter that fires after one or more fields have been removed.
331
+ * @event
332
+ * @param {object} event - The event payload.
333
+ * @param {string[]} event.names - An array of names of the fields that were successfully removed.
334
+ */
335
+ onRemove = new Emitter();
336
+ /**
337
+ * Gets the read-only map of all `Field` instances in this container.
338
+ * @returns {Map<string, Field<any>>} The collection of fields.
339
+ */
340
+ get fields() {
341
+ return this._fields;
342
+ }
343
+ /**
344
+ * Creates an instance of Fields.
345
+ * @param {FieldRegistry} fieldRegistry - The registry used to create new `Field` instances.
346
+ */
347
+ constructor(fieldRegistry) {
348
+ this._fieldRegistry = fieldRegistry;
349
+ }
350
+ /**
351
+ * Checks if a field with the given name exists in the collection.
352
+ * @param {string} name The name of the field to check.
353
+ * @returns {boolean} `true` if the field exists, otherwise `false`.
354
+ */
355
+ has(name) {
356
+ return this._fields.has(name);
357
+ }
358
+ /**
359
+ * Adds a pre-existing `Field` instance to the collection and fires the `onAdd` event.
360
+ * @template T - The specific `Field` type being added.
361
+ * @param {Field<any>} field - The `Field` instance to add.
362
+ * @returns {T} The added `Field` instance, cast to type `T`.
363
+ * @throws If a field with the same name already exists.
364
+ */
365
+ add(field) {
366
+ throwIf(this.has(field.name), `Field with name '${field.name}' already exists`);
367
+ this._fields.set(field.name, field);
368
+ this.onAdd.emit({
369
+ name: field.name,
370
+ field
371
+ });
372
+ return field;
373
+ }
374
+ /**
375
+ * Creates a new `Field` instance of a specified type, adds it to the collection, and returns it.
376
+ * This is the primary factory method for creating fields within this container.
377
+ * @template T - The expected `Field` type to be returned.
378
+ * @param {string} typeName - The registered type name of the field to create (e.g., 'numeric', 'boolean').
379
+ * @param {string} name - The unique name for the new field.
380
+ * @param {*} initialValue - The initial value for the new field.
381
+ * @param {*} [options] - Optional configuration passed to the field's constructor.
382
+ * @returns {T} The newly created `Field` instance.
383
+ */
384
+ create(typeName, name, initialValue, options) {
385
+ const field = new (this._fieldRegistry.get(typeName))(name, initialValue, options);
386
+ this.add(field);
387
+ return field;
388
+ }
389
+ /**
390
+ * Updates an existing field's value or creates a new one if it doesn't exist.
391
+ * @template T - The expected `Field` type.
392
+ * @param {string} typeName - The type name to use if a new field needs to be created.
393
+ * @param {string} name - The name of the field to update or create.
394
+ * @param {*} value - The new value to set.
395
+ * @param {*} [options] - Optional configuration, used only if a new field is created.
396
+ * @returns {T} The existing or newly created `Field` instance.
397
+ */
398
+ upset(typeName, name, value, options) {
399
+ if (this.has(name)) {
400
+ const field = this.get(name);
401
+ field.value = value;
402
+ return field;
403
+ }
404
+ return this.create(typeName, name, value, options);
405
+ }
406
+ /**
407
+ * Retrieves a field by its name.
408
+ * @template TField - The expected `Field` type to be returned.
409
+ * @param {string} name - The name of the field to retrieve.
410
+ * @returns {TField} The `Field` instance.
411
+ * @throws If the field does not exist.
412
+ */
413
+ get(name) {
414
+ throwIf(!this._fields.has(name), `Field with name '${name}' not exists`);
415
+ return this._fields.get(name);
416
+ }
417
+ /**
418
+ * Removes one or more fields from the collection.
419
+ * This method ensures that the `destroy` method of each removed field is called to clean up its resources.
420
+ * @param {string| string[]} names A single name or an array of names to remove.
421
+ */
422
+ remove(names) {
423
+ const reallyRemoved = (Array.isArray(names) ? names : [names]).filter((name) => {
424
+ const field = this._fields.get(name);
425
+ if (!field) return false;
426
+ field.destroy();
427
+ return this._fields.delete(name);
428
+ });
429
+ if (!reallyRemoved.length) return;
430
+ this.onRemove.emit({ names: reallyRemoved });
431
+ }
432
+ /**
433
+ * Removes all fields from the collection, ensuring each is properly destroyed.
434
+ */
435
+ clear() {
436
+ this.remove(Array.from(this._fields.keys()));
437
+ }
438
+ destroy() {
439
+ this.clear();
440
+ this.onAdd.clear();
441
+ this.onRemove.clear();
442
+ }
476
443
  };
477
444
 
478
- // src/field-tree.ts
479
- import { Emitter as Emitter3, ensurePathArray, ensurePathString, throwIf as throwIf3, throwIfEmpty as throwIfEmpty2 } from "@axi-engine/utils";
480
- var FieldTree = class _FieldTree {
481
- static typeName = "fieldTree";
482
- typeName = _FieldTree.typeName;
483
- /** @private The internal map storing child nodes (branches or leaves). */
484
- _nodes = /* @__PURE__ */ new Map();
485
- /** @private The factory used to create new child nodes. */
486
- _factory;
487
- /**
488
- * An event emitter that fires immediately after a new node is added to this tree branch.
489
- * @event
490
- * @param {object} event - The event payload.
491
- * @param {string} event.name - The name (key) of the added node.
492
- * @param event.node - The node instance that was added.
493
- * @example
494
- * myTree.onAdd.subscribe(({ name, node }) => {
495
- * console.log(`Node '${name}' was added.`, node);
496
- * });
497
- */
498
- onAdd = new Emitter3();
499
- /**
500
- * An event emitter that fires once after one or more nodes have been successfully removed.
501
- * @event
502
- * @param {object} event - The event payload.
503
- * @param {string[]} event.names - An array of names of the nodes that were removed.
504
- * @example
505
- * myTree.onRemove.subscribe(({ names }) => {
506
- * console.log(`Nodes removed: ${names.join(', ')}`);
507
- * });
508
- */
509
- onRemove = new Emitter3();
510
- /**
511
- * Gets the collection of direct child nodes of this tree branch.
512
- */
513
- get nodes() {
514
- return this._nodes;
515
- }
516
- /**
517
- * Creates an instance of FieldTree.
518
- * @param {FieldTreeFactory} factory - A factory responsible for creating new nodes within the tree.
519
- */
520
- constructor(factory) {
521
- this._factory = factory;
522
- }
523
- /**
524
- * Checks if a direct child node with the given name exists.
525
- * @param {string} name - The name of the direct child node.
526
- * @returns {boolean} `true` if the node exists, otherwise `false`.
527
- */
528
- has(name) {
529
- return this._nodes.has(name);
530
- }
531
- /**
532
- * Checks if a node exists at a given path, traversing the tree.
533
- * @param {PathType} path - The path to check (e.g., 'player/stats' or ['player', 'stats']).
534
- * @returns {boolean} `true` if the entire path resolves to a node, otherwise `false`.
535
- */
536
- hasPath(path) {
537
- const traversedPath = this.traversePath(path);
538
- return traversedPath.branch.has(traversedPath.leafName);
539
- }
540
- /**
541
- * Adds a pre-existing node as a direct child of this tree branch.
542
- * @param {string} name - The name to assign to the new child node.
543
- * @param {TreeNode} node - The node instance to add.
544
- * @returns {TreeNode} The added node.
545
- * @throws If a node with the same name already exists.
546
- */
547
- addNode(name, node) {
548
- throwIf3(this.has(name), `Can't add node with name: '${name}', node already exists`);
549
- this._nodes.set(name, node);
550
- this.onAdd.emit({ name, node });
551
- return node;
552
- }
553
- /**
554
- * Retrieves a direct child node by its name.
555
- * @param {string} name - The name of the child node.
556
- * @returns {TreeNode} The retrieved node.
557
- * @throws If a node with the given name cannot be found.
558
- */
559
- getNode(name) {
560
- const node = this._nodes.get(name);
561
- throwIfEmpty2(node, `Can't find node with name '${name}'`);
562
- return node;
563
- }
564
- /**
565
- * Removes one or more nodes from this tree branch.
566
- *
567
- * This method first validates that all specified nodes exist. If validation passes,
568
- * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
569
- * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
570
- *
571
- * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
572
- * @throws If any of the specified names do not correspond to an existing node.
573
- */
574
- removeNode(names) {
575
- const toRemoveNames = Array.isArray(names) ? names : [names];
576
- toRemoveNames.forEach((name) => {
577
- throwIf3(!this.has(name), `Can't remove node with name: '${name}', node doesn't exists`);
578
- });
579
- toRemoveNames.forEach((name) => {
580
- this._nodes.get(name).destroy();
581
- this._nodes.delete(name);
582
- });
583
- if (toRemoveNames.length) {
584
- this.onRemove.emit({ names: toRemoveNames });
585
- }
586
- }
587
- /**
588
- * Creates a new `FieldTree` (branch) node at the specified path.
589
- * @param {PathType} path - The path where the new `FieldTree` should be created.
590
- * @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
591
- * @returns {FieldTree} The newly created `FieldTree` instance.
592
- * @throws If the path is invalid or a node already exists at the target location.
593
- */
594
- createFieldTree(path, createPath) {
595
- const traversedPath = this.traversePath(path, createPath);
596
- return traversedPath.branch.addNode(traversedPath.leafName, this._factory.tree());
597
- }
598
- /**
599
- * Creates a new `Fields` (leaf) container at the specified path.
600
- * @param {PathType} path - The path where the new `Fields` container should be created.
601
- * @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
602
- * @returns {Fields} The newly created `Fields` instance.
603
- * @throws If the path is invalid or a node already exists at the target location.
604
- */
605
- createFields(path, createPath) {
606
- const traversedPath = this.traversePath(path, createPath);
607
- return traversedPath.branch.addNode(traversedPath.leafName, this._factory.fields());
608
- }
609
- /**
610
- * Retrieves a `FieldTree` (branch) node from a specified path.
611
- * @param {PathType} path - The path to the `FieldTree` node.
612
- * @returns {FieldTree} The `FieldTree` instance at the specified path.
613
- * @throws If the path is invalid or the node at the path is not a `FieldTree`.
614
- */
615
- getFieldTree(path) {
616
- const traversedPath = this.traversePath(path);
617
- const node = traversedPath.branch.getNode(traversedPath.leafName);
618
- throwIf3(
619
- !(node instanceof _FieldTree),
620
- `Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of FieldTree`
621
- );
622
- return node;
623
- }
624
- /**
625
- * Retrieves a `Fields` (leaf) container from a specified path.
626
- * @param {PathType} path - The path to the `Fields` container.
627
- * @returns {Fields} The `Fields` instance at the specified path.
628
- * @throws If the path is invalid or the node at the path is not a `Fields` container.
629
- */
630
- getFields(path) {
631
- const traversedPath = this.traversePath(path);
632
- const node = traversedPath.branch.getNode(traversedPath.leafName);
633
- throwIf3(
634
- !(node instanceof Fields),
635
- `Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of Fields`
636
- );
637
- return node;
638
- }
639
- /**
640
- * Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
641
- * @param {PathType} path - The path to the `FieldTree` node.
642
- * @returns {FieldTree} The existing or newly created `FieldTree` instance.
643
- */
644
- getOrCreateFieldTree(path) {
645
- const traversedPath = this.traversePath(path, true);
646
- return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFieldTree(traversedPath.leafName) : traversedPath.branch.createFieldTree(traversedPath.leafName);
647
- }
648
- /**
649
- * Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
650
- * @param {PathType} path - The path to the `Fields` container.
651
- * @returns {Fields} The existing or newly created `Fields` instance.
652
- */
653
- getOrCreateFields(path) {
654
- const traversedPath = this.traversePath(path, true);
655
- return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
656
- }
657
- /**
658
- * Finds the parent node for a given path.
659
- * @param path The path to the target node.
660
- * @returns The parent node (either a FieldTree or Fields).
661
- * @throws An error if the path is invalid or any intermediate node is not a FieldTree.
662
- */
663
- findParentNode(path) {
664
- const info = this.traversePath(path);
665
- return info.branch;
666
- }
667
- /**
668
- * Removes all child nodes from this tree branch.
669
- * This method ensures that `destroy()` is called on each child node, allowing for
670
- * a full, recursive cleanup of the entire subtree.
671
- */
672
- clear() {
673
- this.removeNode(Array.from(this._nodes.keys()));
674
- }
675
- /**
676
- * Performs a complete cleanup of this node and its entire subtree.
677
- *
678
- * It recursively destroys all child nodes by calling `clear()` and then
679
- * unsubscribes all listeners from its own event emitters.
680
- * This method should be called when a node is no longer needed.
681
- */
682
- destroy() {
683
- this.clear();
684
- this.onAdd.clear();
685
- this.onRemove.clear();
686
- }
687
- /**
688
- * @private
689
- * Navigates the tree to the parent of a target node.
690
- * This is the core traversal logic for all path-based operations.
691
- * @param {PathType} path - The full path to the target node.
692
- * @param {boolean} [createPath=false] - If `true`, creates missing `FieldTree` branches along the path.
693
- * @returns {{branch: FieldTree, leafName: string}} An object containing the final branch (parent node) and the name of the leaf (target node).
694
- * @throws If the path is empty, invalid, or contains a `Fields` container as an intermediate segment.
695
- */
696
- traversePath(path, createPath) {
697
- const pathArr = ensurePathArray(path);
698
- throwIfEmpty2(pathArr, "The path is empty");
699
- const leafName = pathArr.pop();
700
- let currentNode = this;
701
- for (const pathPart of pathArr) {
702
- let node;
703
- if (currentNode.has(pathPart)) {
704
- node = currentNode.getNode(pathPart);
705
- } else {
706
- if (createPath) {
707
- node = currentNode.createFieldTree(pathPart);
708
- }
709
- }
710
- throwIfEmpty2(node, `Can't find node with name ${pathPart} by path parsing: ${ensurePathString(path)}`);
711
- throwIf3(node instanceof Fields, `Node with name ${pathPart} should be instance of FieldTree`);
712
- currentNode = node;
713
- }
714
- return { branch: currentNode, leafName };
715
- }
445
+ //#endregion
446
+ //#region src/field-tree.ts
447
+ /**
448
+ * Represents a hierarchical data structure for managing the global state of the system.
449
+ *
450
+ * This class acts as the single source of truth for long-term data that exists
451
+ * across different scenes and scripts, such as player stats, inventory,
452
+ * and overall game progress. It uses a path-based system for accessing and
453
+ * manipulating nested data, similar to a file system.
454
+ *
455
+ */
456
+ var FieldTree = class FieldTree {
457
+ static typeName = "fieldTree";
458
+ typeName = FieldTree.typeName;
459
+ /** @private The internal map storing child nodes (branches or leaves). */
460
+ _nodes = /* @__PURE__ */ new Map();
461
+ /** @private The factory used to create new child nodes. */
462
+ _factory;
463
+ /**
464
+ * An event emitter that fires immediately after a new node is added to this tree branch.
465
+ * @event
466
+ * @param {object} event - The event payload.
467
+ * @param {string} event.name - The name (key) of the added node.
468
+ * @param event.node - The node instance that was added.
469
+ * @example
470
+ * myTree.onAdd.subscribe(({ name, node }) => {
471
+ * console.log(`Node '${name}' was added.`, node);
472
+ * });
473
+ */
474
+ onAdd = new Emitter();
475
+ /**
476
+ * An event emitter that fires once after one or more nodes have been successfully removed.
477
+ * @event
478
+ * @param {object} event - The event payload.
479
+ * @param {string[]} event.names - An array of names of the nodes that were removed.
480
+ * @example
481
+ * myTree.onRemove.subscribe(({ names }) => {
482
+ * console.log(`Nodes removed: ${names.join(', ')}`);
483
+ * });
484
+ */
485
+ onRemove = new Emitter();
486
+ /**
487
+ * Gets the collection of direct child nodes of this tree branch.
488
+ */
489
+ get nodes() {
490
+ return this._nodes;
491
+ }
492
+ /**
493
+ * Creates an instance of FieldTree.
494
+ * @param {FieldTreeFactory} factory - A factory responsible for creating new nodes within the tree.
495
+ */
496
+ constructor(factory) {
497
+ this._factory = factory;
498
+ }
499
+ /**
500
+ * Checks if a direct child node with the given name exists.
501
+ * @param {string} name - The name of the direct child node.
502
+ * @returns {boolean} `true` if the node exists, otherwise `false`.
503
+ */
504
+ has(name) {
505
+ return this._nodes.has(name);
506
+ }
507
+ /**
508
+ * Checks if a node exists at a given path, traversing the tree.
509
+ * @param {PathType} path - The path to check (e.g., 'player/stats' or ['player', 'stats']).
510
+ * @returns {boolean} `true` if the entire path resolves to a node, otherwise `false`.
511
+ */
512
+ hasPath(path) {
513
+ const traversedPath = this.traversePath(path);
514
+ return traversedPath.branch.has(traversedPath.leafName);
515
+ }
516
+ /**
517
+ * Adds a pre-existing node as a direct child of this tree branch.
518
+ * @param {string} name - The name to assign to the new child node.
519
+ * @param {TreeNode} node - The node instance to add.
520
+ * @returns {TreeNode} The added node.
521
+ * @throws If a node with the same name already exists.
522
+ */
523
+ addNode(name, node) {
524
+ throwIf(this.has(name), `Can't add node with name: '${name}', node already exists`);
525
+ this._nodes.set(name, node);
526
+ this.onAdd.emit({
527
+ name,
528
+ node
529
+ });
530
+ return node;
531
+ }
532
+ /**
533
+ * Retrieves a direct child node by its name.
534
+ * @param {string} name - The name of the child node.
535
+ * @returns {TreeNode} The retrieved node.
536
+ * @throws If a node with the given name cannot be found.
537
+ */
538
+ getNode(name) {
539
+ const node = this._nodes.get(name);
540
+ throwIfEmpty(node, `Can't find node with name '${name}'`);
541
+ return node;
542
+ }
543
+ /**
544
+ * Removes one or more nodes from this tree branch.
545
+ *
546
+ * This method first validates that all specified nodes exist. If validation passes,
547
+ * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
548
+ * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
549
+ *
550
+ * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
551
+ * @throws If any of the specified names do not correspond to an existing node.
552
+ */
553
+ removeNode(names) {
554
+ const toRemoveNames = Array.isArray(names) ? names : [names];
555
+ toRemoveNames.forEach((name) => {
556
+ throwIf(!this.has(name), `Can't remove node with name: '${name}', node doesn't exists`);
557
+ });
558
+ toRemoveNames.forEach((name) => {
559
+ this._nodes.get(name).destroy();
560
+ this._nodes.delete(name);
561
+ });
562
+ if (toRemoveNames.length) this.onRemove.emit({ names: toRemoveNames });
563
+ }
564
+ /**
565
+ * Creates a new `FieldTree` (branch) node at the specified path.
566
+ * @param {PathType} path - The path where the new `FieldTree` should be created.
567
+ * @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
568
+ * @returns {FieldTree} The newly created `FieldTree` instance.
569
+ * @throws If the path is invalid or a node already exists at the target location.
570
+ */
571
+ createFieldTree(path, createPath) {
572
+ const traversedPath = this.traversePath(path, createPath);
573
+ return traversedPath.branch.addNode(traversedPath.leafName, this._factory.tree());
574
+ }
575
+ /**
576
+ * Creates a new `Fields` (leaf) container at the specified path.
577
+ * @param {PathType} path - The path where the new `Fields` container should be created.
578
+ * @param {boolean} [createPath=false] - If `true`, any missing parent branches in the path will be created automatically.
579
+ * @returns {Fields} The newly created `Fields` instance.
580
+ * @throws If the path is invalid or a node already exists at the target location.
581
+ */
582
+ createFields(path, createPath) {
583
+ const traversedPath = this.traversePath(path, createPath);
584
+ return traversedPath.branch.addNode(traversedPath.leafName, this._factory.fields());
585
+ }
586
+ /**
587
+ * Retrieves a `FieldTree` (branch) node from a specified path.
588
+ * @param {PathType} path - The path to the `FieldTree` node.
589
+ * @returns {FieldTree} The `FieldTree` instance at the specified path.
590
+ * @throws If the path is invalid or the node at the path is not a `FieldTree`.
591
+ */
592
+ getFieldTree(path) {
593
+ const traversedPath = this.traversePath(path);
594
+ const node = traversedPath.branch.getNode(traversedPath.leafName);
595
+ throwIf(!(node instanceof FieldTree), `Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of FieldTree`);
596
+ return node;
597
+ }
598
+ /**
599
+ * Retrieves a `Fields` (leaf) container from a specified path.
600
+ * @param {PathType} path - The path to the `Fields` container.
601
+ * @returns {Fields} The `Fields` instance at the specified path.
602
+ * @throws If the path is invalid or the node at the path is not a `Fields` container.
603
+ */
604
+ getFields(path) {
605
+ const traversedPath = this.traversePath(path);
606
+ const node = traversedPath.branch.getNode(traversedPath.leafName);
607
+ throwIf(!(node instanceof Fields), `Node with name: ${traversedPath.leafName} by path: '${ensurePathString(path)}' should be instance of Fields`);
608
+ return node;
609
+ }
610
+ /**
611
+ * Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
612
+ * @param {PathType} path - The path to the `FieldTree` node.
613
+ * @returns {FieldTree} The existing or newly created `FieldTree` instance.
614
+ */
615
+ getOrCreateFieldTree(path) {
616
+ const traversedPath = this.traversePath(path, true);
617
+ return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFieldTree(traversedPath.leafName) : traversedPath.branch.createFieldTree(traversedPath.leafName);
618
+ }
619
+ /**
620
+ * Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
621
+ * @param {PathType} path - The path to the `Fields` container.
622
+ * @returns {Fields} The existing or newly created `Fields` instance.
623
+ */
624
+ getOrCreateFields(path) {
625
+ const traversedPath = this.traversePath(path, true);
626
+ return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
627
+ }
628
+ /**
629
+ * Finds the parent node for a given path.
630
+ * @param path The path to the target node.
631
+ * @returns The parent node (either a FieldTree or Fields).
632
+ * @throws An error if the path is invalid or any intermediate node is not a FieldTree.
633
+ */
634
+ findParentNode(path) {
635
+ return this.traversePath(path).branch;
636
+ }
637
+ /**
638
+ * Removes all child nodes from this tree branch.
639
+ * This method ensures that `destroy()` is called on each child node, allowing for
640
+ * a full, recursive cleanup of the entire subtree.
641
+ */
642
+ clear() {
643
+ this.removeNode(Array.from(this._nodes.keys()));
644
+ }
645
+ /**
646
+ * Performs a complete cleanup of this node and its entire subtree.
647
+ *
648
+ * It recursively destroys all child nodes by calling `clear()` and then
649
+ * unsubscribes all listeners from its own event emitters.
650
+ * This method should be called when a node is no longer needed.
651
+ */
652
+ destroy() {
653
+ this.clear();
654
+ this.onAdd.clear();
655
+ this.onRemove.clear();
656
+ }
657
+ /**
658
+ * @private
659
+ * Navigates the tree to the parent of a target node.
660
+ * This is the core traversal logic for all path-based operations.
661
+ * @param {PathType} path - The full path to the target node.
662
+ * @param {boolean} [createPath=false] - If `true`, creates missing `FieldTree` branches along the path.
663
+ * @returns {{branch: FieldTree, leafName: string}} An object containing the final branch (parent node) and the name of the leaf (target node).
664
+ * @throws If the path is empty, invalid, or contains a `Fields` container as an intermediate segment.
665
+ */
666
+ traversePath(path, createPath) {
667
+ const pathArr = ensurePathArray(path);
668
+ throwIfEmpty(pathArr, "The path is empty");
669
+ const leafName = pathArr.pop();
670
+ let currentNode = this;
671
+ for (const pathPart of pathArr) {
672
+ let node;
673
+ if (currentNode.has(pathPart)) node = currentNode.getNode(pathPart);
674
+ else if (createPath) node = currentNode.createFieldTree(pathPart);
675
+ throwIfEmpty(node, `Can't find node with name ${pathPart} by path parsing: ${ensurePathString(path)}`);
676
+ throwIf(node instanceof Fields, `Node with name ${pathPart} should be instance of FieldTree`);
677
+ currentNode = node;
678
+ }
679
+ return {
680
+ branch: currentNode,
681
+ leafName
682
+ };
683
+ }
716
684
  };
717
685
 
718
- // src/mixins/with-boolean-fields.mixin.ts
719
- var WithBooleanFields = createTypedMethodsMixin(CoreBooleanField.typeName, "Boolean");
686
+ //#endregion
687
+ //#region src/mixins/with-boolean-fields.mixin.ts
688
+ const WithBooleanFields = createTypedMethodsMixin(CoreBooleanField.typeName, "Boolean");
720
689
 
721
- // src/mixins/with-string-fields.mixin.ts
722
- var WithStringFields = createTypedMethodsMixin(CoreBooleanField.typeName, "String");
690
+ //#endregion
691
+ //#region src/mixins/with-string-fields.mixin.ts
692
+ const WithStringFields = createTypedMethodsMixin(CoreBooleanField.typeName, "String");
723
693
 
724
- // src/mixins/with-numeric-fields.mixin.ts
725
- var WithNumericFields = createTypedMethodsMixin(CoreBooleanField.typeName, "Numeric");
694
+ //#endregion
695
+ //#region src/mixins/with-numeric-fields.mixin.ts
696
+ const WithNumericFields = createTypedMethodsMixin(CoreBooleanField.typeName, "Numeric");
726
697
 
727
- // src/mixins/with-default-generic-fields.mixin.ts
698
+ //#endregion
699
+ //#region src/mixins/with-default-generic-fields.mixin.ts
728
700
  function WithDefaultGenericFields(Base) {
729
- return class FieldsWithDefaultGeneric extends Base {
730
- createGeneric(name, initialValue, options) {
731
- return this.create(CoreField.typeName, name, initialValue, options);
732
- }
733
- upsetGeneric(name, value, options) {
734
- return this.upset(CoreField.typeName, name, value, options);
735
- }
736
- getGeneric(name) {
737
- return this.get(name);
738
- }
739
- };
701
+ return class FieldsWithDefaultGeneric extends Base {
702
+ createGeneric(name, initialValue, options) {
703
+ return this.create(CoreField.typeName, name, initialValue, options);
704
+ }
705
+ upsetGeneric(name, value, options) {
706
+ return this.upset(CoreField.typeName, name, value, options);
707
+ }
708
+ getGeneric(name) {
709
+ return this.get(name);
710
+ }
711
+ };
740
712
  }
741
713
 
742
- // src/core-fields.ts
743
- var CoreFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultGenericFields(Fields)))) {
744
- };
714
+ //#endregion
715
+ //#region src/core-fields.ts
716
+ var CoreFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultGenericFields(Fields)))) {};
745
717
 
746
- // src/core-fields-factory.ts
718
+ //#endregion
719
+ //#region src/core-fields-factory.ts
747
720
  var CoreFieldsFactory = class {
748
- _fieldRegistry;
749
- get fieldRegistry() {
750
- return this._fieldRegistry;
751
- }
752
- constructor(fieldRegistry) {
753
- this._fieldRegistry = fieldRegistry;
754
- }
755
- fields() {
756
- return new CoreFields(this._fieldRegistry);
757
- }
721
+ _fieldRegistry;
722
+ get fieldRegistry() {
723
+ return this._fieldRegistry;
724
+ }
725
+ constructor(fieldRegistry) {
726
+ this._fieldRegistry = fieldRegistry;
727
+ }
728
+ fields() {
729
+ return new CoreFields(this._fieldRegistry);
730
+ }
758
731
  };
759
732
 
760
- // src/core-field-tree.ts
761
- var CoreFieldTree = class extends FieldTree {
762
- };
733
+ //#endregion
734
+ //#region src/core-field-tree.ts
735
+ var CoreFieldTree = class extends FieldTree {};
763
736
 
764
- // src/core-field-tree-factory.ts
737
+ //#endregion
738
+ //#region src/core-field-tree-factory.ts
739
+ /**
740
+ * The default factory implementation that creates standard DefaultFields and FieldTree instances.
741
+ */
765
742
  var CoreTreeNodeFactory = class extends CoreFieldsFactory {
766
- constructor(fieldRegistry) {
767
- super(fieldRegistry);
768
- }
769
- tree() {
770
- return new CoreFieldTree(this);
771
- }
743
+ constructor(fieldRegistry) {
744
+ super(fieldRegistry);
745
+ }
746
+ tree() {
747
+ return new CoreFieldTree(this);
748
+ }
772
749
  };
773
750
 
774
- // src/serializer/policies/clamp-policy-serializer-handler.ts
751
+ //#endregion
752
+ //#region src/serializer/policies/clamp-policy-serializer-handler.ts
775
753
  var ClampPolicySerializerHandler = class {
776
- snapshot(policy) {
777
- return { min: policy.min, max: policy.max };
778
- }
779
- hydrate(data) {
780
- return new ClampPolicy(data.min, data.max);
781
- }
754
+ snapshot(policy) {
755
+ return {
756
+ min: policy.min,
757
+ max: policy.max
758
+ };
759
+ }
760
+ hydrate(data) {
761
+ return new ClampPolicy(data.min, data.max);
762
+ }
782
763
  };
783
764
 
784
- // src/serializer/policies/clamp-max-policy-serializer-handler.ts
765
+ //#endregion
766
+ //#region src/serializer/policies/clamp-max-policy-serializer-handler.ts
785
767
  var ClampMaxPolicySerializerHandler = class {
786
- snapshot(policy) {
787
- return { max: policy.max };
788
- }
789
- hydrate(data) {
790
- return new ClampMaxPolicy(data.max);
791
- }
768
+ snapshot(policy) {
769
+ return { max: policy.max };
770
+ }
771
+ hydrate(data) {
772
+ return new ClampMaxPolicy(data.max);
773
+ }
792
774
  };
793
775
 
794
- // src/serializer/policies/clamp-min-policy-serializer-handler.ts
776
+ //#endregion
777
+ //#region src/serializer/policies/clamp-min-policy-serializer-handler.ts
795
778
  var ClampMinPolicySerializerHandler = class {
796
- snapshot(policy) {
797
- return { min: policy.min };
798
- }
799
- hydrate(data) {
800
- return new ClampMinPolicy(data.min);
801
- }
779
+ snapshot(policy) {
780
+ return { min: policy.min };
781
+ }
782
+ hydrate(data) {
783
+ return new ClampMinPolicy(data.min);
784
+ }
802
785
  };
803
786
 
804
- // src/serializer/policy-serializer.ts
805
- import { throwIf as throwIf4, throwIfEmpty as throwIfEmpty3 } from "@axi-engine/utils";
787
+ //#endregion
788
+ //#region src/serializer/policy-serializer.ts
806
789
  var PolicySerializer = class {
807
- handlers = /* @__PURE__ */ new Map();
808
- register(policyId, handler) {
809
- throwIf4(this.handlers.has(policyId), `A handler for policy ID '${policyId}' is already registered.`);
810
- this.handlers.set(policyId, handler);
811
- return this;
812
- }
813
- clearHandlers() {
814
- this.handlers.clear();
815
- }
816
- /**
817
- * Creates a serializable snapshot of a policy instance.
818
- * The snapshot includes the policy's state and a `__type` identifier.
819
- * @param policy The policy instance to snapshot.
820
- * @returns A plain object ready for JSON serialization.
821
- * @throws If no handler is registered for the policy's ID.
822
- */
823
- snapshot(policy) {
824
- const handler = this.handlers.get(policy.id);
825
- throwIfEmpty3(handler, `No serializer handler registered for policy ID: '${policy.id}'`);
826
- const data = handler.snapshot(policy);
827
- return {
828
- __type: policy.id,
829
- ...data
830
- };
831
- }
832
- /**
833
- * Restores a policy instance from its snapshot representation.
834
- * @param snapshot The plain object snapshot, which must contain a `__type` property.
835
- * @returns A new, fully functional policy instance.
836
- * @throws If the snapshot is invalid or no handler is registered for its `__type`.
837
- */
838
- hydrate(snapshot) {
839
- const typeId = snapshot?.__type;
840
- throwIfEmpty3(typeId, 'Invalid policy snapshot: missing "__type" identifier.');
841
- const handler = this.handlers.get(typeId);
842
- throwIfEmpty3(handler, `No serializer handler registered for policy ID: '${typeId}'`);
843
- const { __type, ...data } = snapshot;
844
- return handler.hydrate(data);
845
- }
790
+ handlers = /* @__PURE__ */ new Map();
791
+ register(policyId, handler) {
792
+ throwIf(this.handlers.has(policyId), `A handler for policy ID '${policyId}' is already registered.`);
793
+ this.handlers.set(policyId, handler);
794
+ return this;
795
+ }
796
+ clearHandlers() {
797
+ this.handlers.clear();
798
+ }
799
+ /**
800
+ * Creates a serializable snapshot of a policy instance.
801
+ * The snapshot includes the policy's state and a `__type` identifier.
802
+ * @param policy The policy instance to snapshot.
803
+ * @returns A plain object ready for JSON serialization.
804
+ * @throws If no handler is registered for the policy's ID.
805
+ */
806
+ snapshot(policy) {
807
+ const handler = this.handlers.get(policy.id);
808
+ throwIfEmpty(handler, `No serializer handler registered for policy ID: '${policy.id}'`);
809
+ const data = handler.snapshot(policy);
810
+ return {
811
+ __type: policy.id,
812
+ ...data
813
+ };
814
+ }
815
+ /**
816
+ * Restores a policy instance from its snapshot representation.
817
+ * @param snapshot The plain object snapshot, which must contain a `__type` property.
818
+ * @returns A new, fully functional policy instance.
819
+ * @throws If the snapshot is invalid or no handler is registered for its `__type`.
820
+ */
821
+ hydrate(snapshot) {
822
+ const typeId = snapshot?.__type;
823
+ throwIfEmpty(typeId, "Invalid policy snapshot: missing \"__type\" identifier.");
824
+ const handler = this.handlers.get(typeId);
825
+ throwIfEmpty(handler, `No serializer handler registered for policy ID: '${typeId}'`);
826
+ const { __type, ...data } = snapshot;
827
+ return handler.hydrate(data);
828
+ }
846
829
  };
847
830
 
848
- // src/serializer/field-serializer.ts
849
- import { isNullOrUndefined as isNullOrUndefined2, throwIfEmpty as throwIfEmpty4 } from "@axi-engine/utils";
831
+ //#endregion
832
+ //#region src/serializer/field-serializer.ts
833
+ /**
834
+ * Orchestrates the serialization and deserialization of Field instances.
835
+ *
836
+ * This class acts as a central point for converting complex Field objects into
837
+ * plain, storable data (snapshots) and vice-versa. It uses a `FieldRegistry`
838
+ * to resolve class constructors and a `PolicySerializer` to handle the state
839
+ * of any attached policies.
840
+ *
841
+ * @todo Implement a `patch(field, snapshot)` method.
842
+ * Unlike `hydrate`, which creates a new
843
+ * instance, `patch` should update the state of an *existing* field instance
844
+ * without breaking external references to it.
845
+ */
850
846
  var FieldSerializer = class {
851
- /**
852
- * Creates an instance of FieldSerializer.
853
- * @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
854
- * @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
855
- */
856
- constructor(fieldRegistry, policySerializer) {
857
- this.fieldRegistry = fieldRegistry;
858
- this.policySerializer = policySerializer;
859
- }
860
- /**
861
- * Creates a serializable snapshot of a Field instance.
862
- * The snapshot includes the field's type, name, current value, and the state of all its policies.
863
- * @param {Field<any>} field - The Field instance to serialize.
864
- * @returns {FieldSnapshot} A plain object ready for JSON serialization.
865
- */
866
- snapshot(field) {
867
- let snapshot = {
868
- __type: field.typeName,
869
- name: field.name,
870
- value: field.value
871
- };
872
- if (!field.policies.isEmpty()) {
873
- const serializedPolicies = [];
874
- field.policies.items.forEach((policy) => serializedPolicies.push(this.policySerializer.snapshot(policy)));
875
- snapshot.policies = serializedPolicies;
876
- }
877
- return snapshot;
878
- }
879
- /**
880
- * Restores a Field instance from its snapshot representation.
881
- * It uses the `__type` property to find the correct constructor and hydrates
882
- * the field with its value and all its policies.
883
- * @param {FieldSnapshot} snapshot - The plain object snapshot to deserialize.
884
- * @returns {Field<any>} A new, fully functional Field instance.
885
- * @throws If the snapshot is invalid, missing a `__type`, or if the type is not registered.
886
- */
887
- hydrate(snapshot) {
888
- const fieldType = snapshot.__type;
889
- throwIfEmpty4(fieldType, 'Invalid field snapshot: missing "__type" identifier.');
890
- const Ctor = this.fieldRegistry.get(fieldType);
891
- let policies;
892
- if (!isNullOrUndefined2(snapshot.policies)) {
893
- policies = [];
894
- snapshot.policies.forEach((p) => policies.push(this.policySerializer.hydrate(p)));
895
- }
896
- return new Ctor(snapshot.name, snapshot.value, { policies });
897
- }
847
+ /**
848
+ * Creates an instance of FieldSerializer.
849
+ * @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
850
+ * @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
851
+ */
852
+ constructor(fieldRegistry, policySerializer) {
853
+ this.fieldRegistry = fieldRegistry;
854
+ this.policySerializer = policySerializer;
855
+ }
856
+ /**
857
+ * Creates a serializable snapshot of a Field instance.
858
+ * The snapshot includes the field's type, name, current value, and the state of all its policies.
859
+ * @param {Field<any>} field - The Field instance to serialize.
860
+ * @returns {FieldSnapshot} A plain object ready for JSON serialization.
861
+ */
862
+ snapshot(field) {
863
+ let snapshot = {
864
+ __type: field.typeName,
865
+ name: field.name,
866
+ value: field.value
867
+ };
868
+ if (!field.policies.isEmpty()) {
869
+ const serializedPolicies = [];
870
+ field.policies.items.forEach((policy) => serializedPolicies.push(this.policySerializer.snapshot(policy)));
871
+ snapshot.policies = serializedPolicies;
872
+ }
873
+ return snapshot;
874
+ }
875
+ /**
876
+ * Restores a Field instance from its snapshot representation.
877
+ * It uses the `__type` property to find the correct constructor and hydrates
878
+ * the field with its value and all its policies.
879
+ * @param {FieldSnapshot} snapshot - The plain object snapshot to deserialize.
880
+ * @returns {Field<any>} A new, fully functional Field instance.
881
+ * @throws If the snapshot is invalid, missing a `__type`, or if the type is not registered.
882
+ */
883
+ hydrate(snapshot) {
884
+ const fieldType = snapshot.__type;
885
+ throwIfEmpty(fieldType, "Invalid field snapshot: missing \"__type\" identifier.");
886
+ const Ctor = this.fieldRegistry.get(fieldType);
887
+ let policies;
888
+ if (!isNullOrUndefined(snapshot.policies)) {
889
+ policies = [];
890
+ snapshot.policies.forEach((p) => policies.push(this.policySerializer.hydrate(p)));
891
+ }
892
+ return new Ctor(snapshot.name, snapshot.value, { policies });
893
+ }
898
894
  };
899
895
 
900
- // src/serializer/fields-serializer.ts
896
+ //#endregion
897
+ //#region src/serializer/fields-serializer.ts
898
+ /**
899
+ * Orchestrates the serialization and deserialization of `Fields` container instances.
900
+ *
901
+ * This class acts as a high-level composer, responsible for converting an entire `Fields` object
902
+ * into a storable snapshot and back.
903
+ * It delegates the actual serialization of each `Field` and `Policy` to their respective serializers.
904
+ *
905
+ * @todo Implement a `patch(fields, snapshot)` method. It should perform a non-destructive
906
+ * update, creating new fields, removing missing ones, and patching existing ones
907
+ * in place, preserving the container instance itself.
908
+ */
901
909
  var FieldsSerializer = class {
902
- /**
903
- * Creates an instance of FieldsSerializer.
904
- * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
905
- * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
906
- */
907
- constructor(fieldsFactory, fieldSerializer) {
908
- this.fieldsFactory = fieldsFactory;
909
- this.fieldSerializer = fieldSerializer;
910
- }
911
- /**
912
- * Creates a serializable snapshot of a `Fields` container.
913
- *
914
- * The snapshot includes a `__type` identifier (currently hardcoded) and an array of snapshots
915
- * for each `Field` within the container.
916
- * @param {Fields} fields - The `Fields` instance to serialize.
917
- * @returns {FieldsSnapshot} A plain object ready for JSON serialization.
918
- */
919
- snapshot(fields) {
920
- const res = {
921
- __type: fields.typeName
922
- };
923
- fields.fields.forEach((field) => res[field.name] = this.fieldSerializer.snapshot(field));
924
- return res;
925
- }
926
- /**
927
- * Restores a `Fields` container instance from its snapshot representation.
928
- *
929
- * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
930
- * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
931
- * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
932
- */
933
- hydrate(snapshot) {
934
- const { __type, ...fieldsData } = snapshot;
935
- const fields = this.fieldsFactory.fields();
936
- for (const fieldName in fieldsData) {
937
- const fieldSnapshot = fieldsData[fieldName];
938
- const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
939
- fields.add(restoredField);
940
- }
941
- return fields;
942
- }
910
+ /**
911
+ * Creates an instance of FieldsSerializer.
912
+ * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
913
+ * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
914
+ */
915
+ constructor(fieldsFactory, fieldSerializer) {
916
+ this.fieldsFactory = fieldsFactory;
917
+ this.fieldSerializer = fieldSerializer;
918
+ }
919
+ /**
920
+ * Creates a serializable snapshot of a `Fields` container.
921
+ *
922
+ * The snapshot includes a `__type` identifier (currently hardcoded) and an array of snapshots
923
+ * for each `Field` within the container.
924
+ * @param {Fields} fields - The `Fields` instance to serialize.
925
+ * @returns {FieldsSnapshot} A plain object ready for JSON serialization.
926
+ */
927
+ snapshot(fields) {
928
+ const res = { __type: fields.typeName };
929
+ fields.fields.forEach((field) => res[field.name] = this.fieldSerializer.snapshot(field));
930
+ return res;
931
+ }
932
+ /**
933
+ * Restores a `Fields` container instance from its snapshot representation.
934
+ *
935
+ * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
936
+ * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
937
+ * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
938
+ */
939
+ hydrate(snapshot) {
940
+ const { __type, ...fieldsData } = snapshot;
941
+ const fields = this.fieldsFactory.fields();
942
+ for (const fieldName in fieldsData) {
943
+ const fieldSnapshot = fieldsData[fieldName];
944
+ const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
945
+ fields.add(restoredField);
946
+ }
947
+ return fields;
948
+ }
943
949
  };
944
950
 
945
- // src/serializer/field-tree-serializer.ts
946
- import { isString } from "@axi-engine/utils";
951
+ //#endregion
952
+ //#region src/serializer/field-tree-serializer.ts
953
+ /**
954
+ * Orchestrates the recursive serialization and deserialization of `FieldTree` instances.
955
+ *
956
+ * This class handles the conversion of an entire `FieldTree` object graph into a
957
+ * plain, storable snapshot and vice-versa. It delegates the processing of `Fields`
958
+ * leaf nodes to a dedicated `FieldsSerializer`.
959
+ * @todo Refactoring: The current implementation uses `if/else` logic in `snapshot` and `hydrate`
960
+ * to process different node types. A more extensible approach would be to use a
961
+ * registry of dedicated handlers for each node type.
962
+ * This would allow new node types to be supported without
963
+ * modifying this class, adhering to the Open/Closed Principle.
964
+ *
965
+ * @todo Implement a `patch(tree, snapshot)` method for recursive, non-destructive
966
+ * updates. This method should traverse the existing tree and the snapshot,
967
+ * patching nodes in place to maintain object references.
968
+ */
947
969
  var FieldTreeSerializer = class {
948
- constructor(fieldTreeNodeFactory, fieldsSerializer) {
949
- this.fieldTreeNodeFactory = fieldTreeNodeFactory;
950
- this.fieldsSerializer = fieldsSerializer;
951
- }
952
- /**
953
- * Creates a serializable snapshot of the entire tree and its contained fields.
954
- * @returns A plain JavaScript object representing the complete state managed by this tree.
955
- */
956
- snapshot(tree) {
957
- const res = {
958
- __type: tree.typeName
959
- };
960
- tree.nodes.forEach((node, key) => {
961
- if (node.typeName === tree.typeName) {
962
- res[key] = this.snapshot(node);
963
- } else if (node.typeName === Fields.typeName) {
964
- res[key] = this.fieldsSerializer.snapshot(node);
965
- }
966
- });
967
- return res;
968
- }
969
- /**
970
- * Restores the state of the tree from a snapshot.
971
- * It intelligently creates missing nodes based on `__type` metadata and delegates hydration to child nodes.
972
- * @param snapshot The snapshot object to load.
973
- */
974
- hydrate(snapshot) {
975
- const { __type, ...nodes } = snapshot;
976
- const tree = this.fieldTreeNodeFactory.tree();
977
- for (const key in nodes) {
978
- const nodeData = nodes[key];
979
- if (isString(nodeData)) {
980
- continue;
981
- }
982
- if (nodeData.__type === FieldTree.typeName) {
983
- tree.addNode(key, this.hydrate(nodeData));
984
- } else if (nodeData.__type === Fields.typeName) {
985
- tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
986
- }
987
- }
988
- return tree;
989
- }
970
+ constructor(fieldTreeNodeFactory, fieldsSerializer) {
971
+ this.fieldTreeNodeFactory = fieldTreeNodeFactory;
972
+ this.fieldsSerializer = fieldsSerializer;
973
+ }
974
+ /**
975
+ * Creates a serializable snapshot of the entire tree and its contained fields.
976
+ * @returns A plain JavaScript object representing the complete state managed by this tree.
977
+ */
978
+ snapshot(tree) {
979
+ const res = { __type: tree.typeName };
980
+ tree.nodes.forEach((node, key) => {
981
+ if (node.typeName === tree.typeName) res[key] = this.snapshot(node);
982
+ else if (node.typeName === Fields.typeName) res[key] = this.fieldsSerializer.snapshot(node);
983
+ });
984
+ return res;
985
+ }
986
+ /**
987
+ * Restores the state of the tree from a snapshot.
988
+ * It intelligently creates missing nodes based on `__type` metadata and delegates hydration to child nodes.
989
+ * @param snapshot The snapshot object to load.
990
+ */
991
+ hydrate(snapshot) {
992
+ const { __type, ...nodes } = snapshot;
993
+ const tree = this.fieldTreeNodeFactory.tree();
994
+ for (const key in nodes) {
995
+ const nodeData = nodes[key];
996
+ if (isString(nodeData)) continue;
997
+ if (nodeData.__type === FieldTree.typeName) tree.addNode(key, this.hydrate(nodeData));
998
+ else if (nodeData.__type === Fields.typeName) tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
999
+ }
1000
+ return tree;
1001
+ }
990
1002
  };
991
1003
 
992
- // src/data-store-field-resolver.ts
993
- import { isBoolean, isNumber, isString as isString2 } from "@axi-engine/utils";
1004
+ //#endregion
1005
+ //#region src/data-store-field-resolver.ts
994
1006
  var NumericFieldResolver = class {
995
- typeName = CoreNumericField.typeName;
996
- supports(value) {
997
- return isNumber(value);
998
- }
1007
+ typeName = CoreNumericField.typeName;
1008
+ supports(value) {
1009
+ return isNumber(value);
1010
+ }
999
1011
  };
1000
1012
  var BooleanFieldResolver = class {
1001
- typeName = CoreBooleanField.typeName;
1002
- supports(value) {
1003
- return isBoolean(value);
1004
- }
1013
+ typeName = CoreBooleanField.typeName;
1014
+ supports(value) {
1015
+ return isBoolean(value);
1016
+ }
1005
1017
  };
1006
1018
  var StringFieldResolver = class {
1007
- typeName = CoreStringField.typeName;
1008
- supports(value) {
1009
- return isString2(value);
1010
- }
1019
+ typeName = CoreStringField.typeName;
1020
+ supports(value) {
1021
+ return isString(value);
1022
+ }
1011
1023
  };
1012
1024
 
1013
- // src/data-store.ts
1014
- import { ensurePathArray as ensurePathArray2, ensurePathString as ensurePathString2, throwIfEmpty as throwIfEmpty5 } from "@axi-engine/utils";
1025
+ //#endregion
1026
+ //#region src/data-store.ts
1015
1027
  var DataStore = class {
1016
- constructor(tree) {
1017
- this.tree = tree;
1018
- this.registerResolver(new NumericFieldResolver());
1019
- this.registerResolver(new BooleanFieldResolver());
1020
- this.registerResolver(new StringFieldResolver());
1021
- }
1022
- resolvers = [];
1023
- rootFieldsName = "__root_fields";
1024
- _rootFields;
1025
- get rootFields() {
1026
- if (!this._rootFields) {
1027
- this._rootFields = this.tree.getOrCreateFields(this.rootFieldsName);
1028
- }
1029
- return this._rootFields;
1030
- }
1031
- registerResolver(resolver) {
1032
- this.resolvers.unshift(resolver);
1033
- }
1034
- clearResolvers() {
1035
- this.resolvers.length = 0;
1036
- }
1037
- getValue(path) {
1038
- return this.getField(path).value;
1039
- }
1040
- setValue(path, val) {
1041
- const field = this.getField(path);
1042
- field.value = val;
1043
- return field.value;
1044
- }
1045
- createValue(path, val, options) {
1046
- const dest = this.getDestinationFields(path);
1047
- if (options?.fieldType) {
1048
- return dest.fields.create(options.fieldType, dest.leafName, val, options).value;
1049
- }
1050
- for (let resolver of this.resolvers) {
1051
- if (resolver.supports(val)) {
1052
- return dest.fields.create(resolver.typeName, dest.leafName, val, options).value;
1053
- }
1054
- }
1055
- return dest.fields.createGeneric(dest.leafName, val, options).value;
1056
- }
1057
- upsetValue(path, val, options) {
1058
- const dest = this.getDestinationFields(path);
1059
- if (options?.fieldType) {
1060
- return dest.fields.upset(options.fieldType, dest.leafName, val, options).value;
1061
- }
1062
- for (let resolver of this.resolvers) {
1063
- if (resolver.supports(val)) {
1064
- return dest.fields.upset(resolver.typeName, dest.leafName, val, options).value;
1065
- }
1066
- }
1067
- return dest.fields.upsetGeneric(dest.leafName, val, options).value;
1068
- }
1069
- createBoolean(path, initialValue, options) {
1070
- const dest = this.getDestinationFields(path);
1071
- return dest.fields.createBoolean(dest.leafName, initialValue, options);
1072
- }
1073
- createNumeric(path, initialValue, options) {
1074
- const dest = this.getDestinationFields(path);
1075
- return dest.fields.createNumeric(dest.leafName, initialValue, options);
1076
- }
1077
- createString(path, initialValue, options) {
1078
- const dest = this.getDestinationFields(path);
1079
- return dest.fields.createString(dest.leafName, initialValue, options);
1080
- }
1081
- createGeneric(path, initialValue, options) {
1082
- const dest = this.getDestinationFields(path);
1083
- return dest.fields.createGeneric(dest.leafName, initialValue, options);
1084
- }
1085
- getBoolean(path) {
1086
- return this.getField(path);
1087
- }
1088
- getNumeric(path) {
1089
- return this.getField(path);
1090
- }
1091
- getString(path) {
1092
- return this.getField(path);
1093
- }
1094
- getGeneric(path) {
1095
- return this.getField(path);
1096
- }
1097
- getField(path) {
1098
- const pathArr = ensurePathArray2(path);
1099
- throwIfEmpty5(pathArr, `Wrong path or path is empty: ${ensurePathString2(path)}, should contain at least one path segment`);
1100
- if (this.isPathToRootFields(pathArr)) {
1101
- return this.rootFields.get(pathArr[0]);
1102
- }
1103
- const fieldName = pathArr.pop();
1104
- const fields = this.tree.getFields(pathArr);
1105
- return fields.get(fieldName);
1106
- }
1107
- createFields(path) {
1108
- return this.tree.createFields(path, true);
1109
- }
1110
- createTree(path) {
1111
- return this.tree.createFieldTree(path, true);
1112
- }
1113
- getFields(path) {
1114
- return this.tree.getFields(path);
1115
- }
1116
- getTree(path) {
1117
- return this.tree.getFieldTree(path);
1118
- }
1119
- remove(path) {
1120
- const pathArr = ensurePathArray2(path);
1121
- throwIfEmpty5(pathArr, `Wrong path or path is empty: ${ensurePathString2(path)}, should contain at least one path segment`);
1122
- if (this.isPathToRootFields(pathArr)) {
1123
- this.rootFields.remove(pathArr);
1124
- return;
1125
- }
1126
- const node = this.tree.findParentNode(pathArr);
1127
- const leafName = pathArr[pathArr.length - 1];
1128
- if (node instanceof CoreFields) {
1129
- node.remove(leafName);
1130
- } else if (node instanceof CoreFieldTree) {
1131
- node.removeNode(leafName);
1132
- }
1133
- }
1134
- isPathToRootFields(path) {
1135
- return ensurePathArray2(path).length === 1;
1136
- }
1137
- getDestinationFields(path) {
1138
- const pathArr = ensurePathArray2(path);
1139
- if (this.isPathToRootFields(pathArr)) {
1140
- return { fields: this.rootFields, leafName: pathArr[0] };
1141
- }
1142
- const leafName = pathArr.pop();
1143
- return { fields: this.tree.getOrCreateFields(path), leafName };
1144
- }
1028
+ resolvers = [];
1029
+ rootFieldsName = "__root_fields";
1030
+ _rootFields;
1031
+ get rootFields() {
1032
+ if (!this._rootFields) this._rootFields = this.tree.getOrCreateFields(this.rootFieldsName);
1033
+ return this._rootFields;
1034
+ }
1035
+ constructor(tree) {
1036
+ this.tree = tree;
1037
+ this.registerResolver(new NumericFieldResolver());
1038
+ this.registerResolver(new BooleanFieldResolver());
1039
+ this.registerResolver(new StringFieldResolver());
1040
+ }
1041
+ registerResolver(resolver) {
1042
+ this.resolvers.unshift(resolver);
1043
+ }
1044
+ clearResolvers() {
1045
+ this.resolvers.length = 0;
1046
+ }
1047
+ getValue(path) {
1048
+ return this.getField(path).value;
1049
+ }
1050
+ setValue(path, val) {
1051
+ /** for case when field has policies */
1052
+ const field = this.getField(path);
1053
+ field.value = val;
1054
+ return field.value;
1055
+ }
1056
+ createValue(path, val, options) {
1057
+ const dest = this.getDestinationFields(path);
1058
+ if (options?.fieldType) return dest.fields.create(options.fieldType, dest.leafName, val, options).value;
1059
+ for (let resolver of this.resolvers) if (resolver.supports(val)) return dest.fields.create(resolver.typeName, dest.leafName, val, options).value;
1060
+ return dest.fields.createGeneric(dest.leafName, val, options).value;
1061
+ }
1062
+ upsetValue(path, val, options) {
1063
+ const dest = this.getDestinationFields(path);
1064
+ if (options?.fieldType) return dest.fields.upset(options.fieldType, dest.leafName, val, options).value;
1065
+ for (let resolver of this.resolvers) if (resolver.supports(val)) return dest.fields.upset(resolver.typeName, dest.leafName, val, options).value;
1066
+ return dest.fields.upsetGeneric(dest.leafName, val, options).value;
1067
+ }
1068
+ createBoolean(path, initialValue, options) {
1069
+ const dest = this.getDestinationFields(path);
1070
+ return dest.fields.createBoolean(dest.leafName, initialValue, options);
1071
+ }
1072
+ createNumeric(path, initialValue, options) {
1073
+ const dest = this.getDestinationFields(path);
1074
+ return dest.fields.createNumeric(dest.leafName, initialValue, options);
1075
+ }
1076
+ createString(path, initialValue, options) {
1077
+ const dest = this.getDestinationFields(path);
1078
+ return dest.fields.createString(dest.leafName, initialValue, options);
1079
+ }
1080
+ createGeneric(path, initialValue, options) {
1081
+ const dest = this.getDestinationFields(path);
1082
+ return dest.fields.createGeneric(dest.leafName, initialValue, options);
1083
+ }
1084
+ getBoolean(path) {
1085
+ return this.getField(path);
1086
+ }
1087
+ getNumeric(path) {
1088
+ return this.getField(path);
1089
+ }
1090
+ getString(path) {
1091
+ return this.getField(path);
1092
+ }
1093
+ getGeneric(path) {
1094
+ return this.getField(path);
1095
+ }
1096
+ getField(path) {
1097
+ const pathArr = ensurePathArray(path);
1098
+ throwIfEmpty(pathArr, `Wrong path or path is empty: ${ensurePathString(path)}, should contain at least one path segment`);
1099
+ if (this.isPathToRootFields(pathArr)) return this.rootFields.get(pathArr[0]);
1100
+ const fieldName = pathArr.pop();
1101
+ return this.tree.getFields(pathArr).get(fieldName);
1102
+ }
1103
+ createFields(path) {
1104
+ return this.tree.createFields(path, true);
1105
+ }
1106
+ createTree(path) {
1107
+ return this.tree.createFieldTree(path, true);
1108
+ }
1109
+ getFields(path) {
1110
+ return this.tree.getFields(path);
1111
+ }
1112
+ getTree(path) {
1113
+ return this.tree.getFieldTree(path);
1114
+ }
1115
+ remove(path) {
1116
+ const pathArr = ensurePathArray(path);
1117
+ throwIfEmpty(pathArr, `Wrong path or path is empty: ${ensurePathString(path)}, should contain at least one path segment`);
1118
+ /** remove field from root fields */
1119
+ if (this.isPathToRootFields(pathArr)) {
1120
+ this.rootFields.remove(pathArr);
1121
+ return;
1122
+ }
1123
+ const node = this.tree.findParentNode(pathArr);
1124
+ const leafName = pathArr[pathArr.length - 1];
1125
+ if (node instanceof CoreFields) node.remove(leafName);
1126
+ else if (node instanceof CoreFieldTree) node.removeNode(leafName);
1127
+ }
1128
+ isPathToRootFields(path) {
1129
+ return ensurePathArray(path).length === 1;
1130
+ }
1131
+ getDestinationFields(path) {
1132
+ const pathArr = ensurePathArray(path);
1133
+ if (this.isPathToRootFields(pathArr)) return {
1134
+ fields: this.rootFields,
1135
+ leafName: pathArr[0]
1136
+ };
1137
+ const leafName = pathArr.pop();
1138
+ return {
1139
+ fields: this.tree.getOrCreateFields(path),
1140
+ leafName
1141
+ };
1142
+ }
1145
1143
  };
1146
1144
 
1147
- // src/setup.ts
1145
+ //#endregion
1146
+ //#region src/setup.ts
1147
+ /**
1148
+ * Creates and configures a FieldRegistry with all the core field types.
1149
+ * @returns {FieldRegistry} A pre-configured FieldRegistry instance.
1150
+ */
1148
1151
  function createCoreFieldRegistry() {
1149
- const fieldRegistry = new FieldRegistry();
1150
- fieldRegistry.register(CoreField.typeName, CoreField);
1151
- fieldRegistry.register(CoreNumericField.typeName, CoreNumericField);
1152
- fieldRegistry.register(CoreStringField.typeName, CoreStringField);
1153
- fieldRegistry.register(CoreBooleanField.typeName, CoreBooleanField);
1154
- return fieldRegistry;
1152
+ const fieldRegistry = new FieldRegistry();
1153
+ fieldRegistry.register(CoreField.typeName, CoreField);
1154
+ fieldRegistry.register(CoreNumericField.typeName, CoreNumericField);
1155
+ fieldRegistry.register(CoreStringField.typeName, CoreStringField);
1156
+ fieldRegistry.register(CoreBooleanField.typeName, CoreBooleanField);
1157
+ return fieldRegistry;
1155
1158
  }
1159
+ /**
1160
+ * Creates and configures a PolicySerializer with handlers for core policies.
1161
+ * @returns {PolicySerializer} A pre-configured PolicySerializer instance.
1162
+ */
1156
1163
  function createCorePolicySerializer() {
1157
- const policySerializer = new PolicySerializer();
1158
- policySerializer.register(ClampPolicy.id, new ClampPolicySerializerHandler());
1159
- policySerializer.register(ClampMinPolicy.id, new ClampMinPolicySerializerHandler());
1160
- policySerializer.register(ClampMaxPolicy.id, new ClampMaxPolicySerializerHandler());
1161
- return policySerializer;
1164
+ const policySerializer = new PolicySerializer();
1165
+ policySerializer.register(ClampPolicy.id, new ClampPolicySerializerHandler());
1166
+ policySerializer.register(ClampMinPolicy.id, new ClampMinPolicySerializerHandler());
1167
+ policySerializer.register(ClampMaxPolicy.id, new ClampMaxPolicySerializerHandler());
1168
+ return policySerializer;
1162
1169
  }
1170
+ /**
1171
+ * Creates a factory for CoreFieldTree and CoreFields nodes.
1172
+ * @param {FieldRegistry} fieldRegistry - The registry to be used by the factory.
1173
+ * @returns {CoreTreeNodeFactory} A new CoreTreeNodeFactory instance.
1174
+ */
1163
1175
  function createCoreTreeNodeFactory(fieldRegistry) {
1164
- return new CoreTreeNodeFactory(fieldRegistry);
1176
+ return new CoreTreeNodeFactory(fieldRegistry);
1165
1177
  }
1178
+ /**
1179
+ * Creates a fully configured serializer for a FieldTree.
1180
+ * This function composes all necessary serializers (FieldTree, Fields, Field) for a complete setup.
1181
+ * @param {CoreTreeNodeFactory} fieldTreeNodeFactory - The factory used to create new tree nodes during deserialization.
1182
+ * @param policySerializer
1183
+ * @returns {FieldTreeSerializer<CoreFields>} A top-level serializer for the entire field tree.
1184
+ */
1166
1185
  function createCoreTreeSerializer(fieldTreeNodeFactory, policySerializer) {
1167
- return new FieldTreeSerializer(
1168
- fieldTreeNodeFactory,
1169
- new FieldsSerializer(
1170
- fieldTreeNodeFactory,
1171
- new FieldSerializer(fieldTreeNodeFactory.fieldRegistry, policySerializer ?? createCorePolicySerializer())
1172
- )
1173
- );
1186
+ return new FieldTreeSerializer(fieldTreeNodeFactory, new FieldsSerializer(fieldTreeNodeFactory, new FieldSerializer(fieldTreeNodeFactory.fieldRegistry, policySerializer ?? createCorePolicySerializer())));
1174
1187
  }
1188
+ /**
1189
+ * Creates a complete core setup for the field system.
1190
+ * @returns {{factory: CoreTreeNodeFactory, serializer: FieldTreeSerializer<CoreFields>}}
1191
+ */
1175
1192
  function createCoreFieldSystem(config) {
1176
- const registry = config?.registry ?? createCoreFieldRegistry();
1177
- const factory = createCoreTreeNodeFactory(registry);
1178
- const serializer = createCoreTreeSerializer(factory, config?.policySerializer);
1179
- return { factory, serializer };
1193
+ const factory = createCoreTreeNodeFactory(config?.registry ?? createCoreFieldRegistry());
1194
+ return {
1195
+ factory,
1196
+ serializer: createCoreTreeSerializer(factory, config?.policySerializer)
1197
+ };
1180
1198
  }
1181
- export {
1182
- ClampMaxPolicy,
1183
- ClampMaxPolicySerializerHandler,
1184
- ClampMinPolicy,
1185
- ClampMinPolicySerializerHandler,
1186
- ClampPolicy,
1187
- ClampPolicySerializerHandler,
1188
- CoreBooleanField,
1189
- CoreField,
1190
- CoreFieldTree,
1191
- CoreFields,
1192
- CoreFieldsFactory,
1193
- CoreNumericField,
1194
- CoreStringField,
1195
- CoreTreeNodeFactory,
1196
- DataStore,
1197
- FieldRegistry,
1198
- FieldSerializer,
1199
- FieldTree,
1200
- FieldTreeSerializer,
1201
- Fields,
1202
- FieldsSerializer,
1203
- Policies,
1204
- PolicySerializer,
1205
- clampMaxPolicy,
1206
- clampMinPolicy,
1207
- clampPolicy,
1208
- createCoreFieldRegistry,
1209
- createCoreFieldSystem,
1210
- createCorePolicySerializer,
1211
- createCoreTreeNodeFactory,
1212
- createCoreTreeSerializer,
1213
- createTypedMethodsMixin
1214
- };
1199
+
1200
+ //#endregion
1201
+ export { ClampMaxPolicy, ClampMaxPolicySerializerHandler, ClampMinPolicy, ClampMinPolicySerializerHandler, ClampPolicy, ClampPolicySerializerHandler, CoreBooleanField, CoreField, CoreFieldTree, CoreFields, CoreFieldsFactory, CoreNumericField, CoreStringField, CoreTreeNodeFactory, DataStore, FieldRegistry, FieldSerializer, FieldTree, FieldTreeSerializer, Fields, FieldsSerializer, Policies, PolicySerializer, clampMaxPolicy, clampMinPolicy, clampPolicy, createCoreFieldRegistry, createCoreFieldSystem, createCorePolicySerializer, createCoreTreeNodeFactory, createCoreTreeSerializer, createTypedMethodsMixin };
1202
+ //# sourceMappingURL=index.mjs.map