@axi-engine/fields 0.1.5 → 0.2.0

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