@axi-engine/fields 0.2.0 → 0.2.2

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.d.mts CHANGED
@@ -330,6 +330,7 @@ declare class Fields {
330
330
  * Removes all fields from the collection, ensuring each is properly destroyed.
331
331
  */
332
332
  clear(): void;
333
+ destroy(): void;
333
334
  }
334
335
 
335
336
  declare const DefaultFields_base: {
@@ -355,6 +356,7 @@ declare const DefaultFields_base: {
355
356
  get<T extends Field<any>>(name: string): T;
356
357
  remove(names: string | string[]): void;
357
358
  clear(): void;
359
+ destroy(): void;
358
360
  };
359
361
  } & {
360
362
  new (...args: any[]): {
@@ -379,6 +381,7 @@ declare const DefaultFields_base: {
379
381
  get<T extends Field<any>>(name: string): T;
380
382
  remove(names: string | string[]): void;
381
383
  clear(): void;
384
+ destroy(): void;
382
385
  };
383
386
  } & {
384
387
  new (...args: any[]): {
@@ -403,11 +406,13 @@ declare const DefaultFields_base: {
403
406
  get<T extends Field<any>>(name: string): T;
404
407
  remove(names: string | string[]): void;
405
408
  clear(): void;
409
+ destroy(): void;
406
410
  };
407
411
  } & {
408
412
  new (...args: any[]): {
409
- createDefault<T>(name: string, initialValue: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
410
- upsetDefault<T>(name: string, value: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
413
+ createGeneric<T>(name: string, initialValue: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
414
+ upsetGeneric<T>(name: string, value: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
415
+ getGeneric<T>(name: string): DefaultField<T>;
411
416
  readonly typeName: "fields";
412
417
  readonly _fields: Map<string, Field<any>>;
413
418
  readonly _fieldRegistry: FieldRegistry;
@@ -426,31 +431,38 @@ declare const DefaultFields_base: {
426
431
  get<T extends Field<any>>(name: string): T;
427
432
  remove(names: string | string[]): void;
428
433
  clear(): void;
434
+ destroy(): void;
429
435
  };
430
436
  } & typeof Fields;
431
437
  declare class DefaultFields extends DefaultFields_base {
432
438
  }
433
439
 
440
+ interface FieldsFactory<TFields extends Fields> {
441
+ fields(): TFields;
442
+ }
443
+ declare class DefaultFieldsFactory implements FieldsFactory<DefaultFields> {
444
+ private readonly fieldRegistry;
445
+ constructor(fieldRegistry: FieldRegistry);
446
+ fields(): DefaultFields;
447
+ }
434
448
  /**
435
449
  * Defines the contract for a factory that creates nodes for a FieldTree.
436
450
  * This allows for custom implementations of Fields and FieldTree to be used.
437
451
  */
438
- interface TreeNodeFactory {
439
- fields<T extends Fields>(): T;
440
- tree<T extends FieldTree>(): T;
452
+ interface TreeNodeFactory<TFields extends Fields> extends FieldsFactory<TFields> {
453
+ fields(): TFields;
454
+ tree(): FieldTree<TFields>;
441
455
  }
442
456
  /**
443
457
  * The default factory implementation that creates standard DefaultFields and FieldTree instances.
444
458
  */
445
- declare class DefaultTreeNodeFactory implements TreeNodeFactory {
446
- private readonly fieldRegistry;
459
+ declare class DefaultTreeNodeFactory extends DefaultFieldsFactory implements TreeNodeFactory<DefaultFields> {
447
460
  constructor(fieldRegistry: FieldRegistry);
448
- fields: () => any;
449
- tree: () => any;
461
+ tree(): FieldTree<DefaultFields>;
450
462
  }
451
463
 
452
464
  /** A type alias for any container that can be a child node in a FieldTree */
453
- type TreeOrFieldsNode = FieldTree | Fields;
465
+ type TreeNode<F extends Fields> = FieldTree<F> | F;
454
466
  /**
455
467
  * Represents a hierarchical data structure for managing the global state of the system.
456
468
  *
@@ -459,26 +471,51 @@ type TreeOrFieldsNode = FieldTree | Fields;
459
471
  * and overall game progress. It uses a path-based system for accessing and
460
472
  * manipulating nested data, similar to a file system.
461
473
  *
462
- * @todo
463
- * - Add node removal functionality.
464
- * - Implement an event system for node creation/removal.
465
474
  */
466
- declare class FieldTree {
475
+ declare class FieldTree<TFields extends Fields> {
467
476
  static readonly typeName = "fieldTree";
468
477
  readonly typeName = "fieldTree";
469
478
  /** @private The internal map storing child nodes (branches or leaves). */
470
479
  private readonly _nodes;
471
480
  /** @private The factory used to create new child nodes. */
472
481
  private readonly _factory;
482
+ /**
483
+ * An event emitter that fires immediately after a new node is added to this tree branch.
484
+ * @event
485
+ * @param {object} event - The event payload.
486
+ * @param {string} event.name - The name (key) of the added node.
487
+ * @param event.node - The node instance that was added.
488
+ * @example
489
+ * myTree.onAdd.subscribe(({ name, node }) => {
490
+ * console.log(`Node '${name}' was added.`, node);
491
+ * });
492
+ */
493
+ onAdd: Emitter<[event: {
494
+ name: string;
495
+ node: TreeNode<TFields>;
496
+ }]>;
497
+ /**
498
+ * An event emitter that fires once after one or more nodes have been successfully removed.
499
+ * @event
500
+ * @param {object} event - The event payload.
501
+ * @param {string[]} event.names - An array of names of the nodes that were removed.
502
+ * @example
503
+ * myTree.onRemove.subscribe(({ names }) => {
504
+ * console.log(`Nodes removed: ${names.join(', ')}`);
505
+ * });
506
+ */
507
+ onRemove: Emitter<[event: {
508
+ names: string[];
509
+ }]>;
473
510
  /**
474
511
  * Gets the collection of direct child nodes of this tree branch.
475
512
  */
476
- get nodes(): Map<string, TreeOrFieldsNode>;
513
+ get nodes(): Map<string, TreeNode<TFields>>;
477
514
  /**
478
515
  * Creates an instance of FieldTree.
479
516
  * @param {TreeNodeFactory} factory - A factory responsible for creating new nodes within the tree.
480
517
  */
481
- constructor(factory: TreeNodeFactory);
518
+ constructor(factory: TreeNodeFactory<TFields>);
482
519
  /**
483
520
  * Checks if a direct child node with the given name exists.
484
521
  * @param {string} name - The name of the direct child node.
@@ -494,18 +531,29 @@ declare class FieldTree {
494
531
  /**
495
532
  * Adds a pre-existing node as a direct child of this tree branch.
496
533
  * @param {string} name - The name to assign to the new child node.
497
- * @param {TreeOrFieldsNode} node - The node instance to add.
498
- * @returns {TreeOrFieldsNode} The added node.
534
+ * @param {TreeNode} node - The node instance to add.
535
+ * @returns {TreeNode} The added node.
499
536
  * @throws If a node with the same name already exists.
500
537
  */
501
- addNode(name: string, node: TreeOrFieldsNode): TreeOrFieldsNode;
538
+ addNode(name: string, node: TreeNode<TFields>): TreeNode<TFields>;
502
539
  /**
503
540
  * Retrieves a direct child node by its name.
504
541
  * @param {string} name - The name of the child node.
505
- * @returns {TreeOrFieldsNode} The retrieved node.
542
+ * @returns {TreeNode} The retrieved node.
506
543
  * @throws If a node with the given name cannot be found.
507
544
  */
508
- getNode(name: string): TreeOrFieldsNode;
545
+ getNode(name: string): TreeNode<TFields>;
546
+ /**
547
+ * Removes one or more nodes from this tree branch.
548
+ *
549
+ * This method first validates that all specified nodes exist. If validation passes,
550
+ * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
551
+ * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
552
+ *
553
+ * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
554
+ * @throws If any of the specified names do not correspond to an existing node.
555
+ */
556
+ removeNode(names: string | string[]): void;
509
557
  /**
510
558
  * Creates a new `FieldTree` (branch) node at the specified path.
511
559
  * @param {PathType} path - The path where the new `FieldTree` should be created.
@@ -513,7 +561,7 @@ declare class FieldTree {
513
561
  * @returns {FieldTree} The newly created `FieldTree` instance.
514
562
  * @throws If the path is invalid or a node already exists at the target location.
515
563
  */
516
- createFieldTree<T extends FieldTree>(path: PathType, createPath?: boolean): T;
564
+ createFieldTree<T extends FieldTree<TFields>>(path: PathType, createPath?: boolean): T;
517
565
  /**
518
566
  * Creates a new `Fields` (leaf) container at the specified path.
519
567
  * @param {PathType} path - The path where the new `Fields` container should be created.
@@ -521,33 +569,47 @@ declare class FieldTree {
521
569
  * @returns {Fields} The newly created `Fields` instance.
522
570
  * @throws If the path is invalid or a node already exists at the target location.
523
571
  */
524
- createFields<T extends Fields>(path: PathType, createPath?: boolean): T;
572
+ createFields(path: PathType, createPath?: boolean): TFields;
525
573
  /**
526
574
  * Retrieves a `FieldTree` (branch) node from a specified path.
527
575
  * @param {PathType} path - The path to the `FieldTree` node.
528
576
  * @returns {FieldTree} The `FieldTree` instance at the specified path.
529
577
  * @throws If the path is invalid or the node at the path is not a `FieldTree`.
530
578
  */
531
- getFieldTree(path: PathType): FieldTree;
579
+ getFieldTree(path: PathType): FieldTree<TFields>;
532
580
  /**
533
581
  * Retrieves a `Fields` (leaf) container from a specified path.
534
582
  * @param {PathType} path - The path to the `Fields` container.
535
583
  * @returns {Fields} The `Fields` instance at the specified path.
536
584
  * @throws If the path is invalid or the node at the path is not a `Fields` container.
537
585
  */
538
- getFields(path: PathType): Fields;
586
+ getFields(path: PathType): TFields;
539
587
  /**
540
588
  * Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
541
589
  * @param {PathType} path - The path to the `FieldTree` node.
542
590
  * @returns {FieldTree} The existing or newly created `FieldTree` instance.
543
591
  */
544
- getOrCreateFieldTree(path: PathType): FieldTree;
592
+ getOrCreateFieldTree(path: PathType): FieldTree<TFields>;
545
593
  /**
546
594
  * Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
547
595
  * @param {PathType} path - The path to the `Fields` container.
548
596
  * @returns {Fields} The existing or newly created `Fields` instance.
549
597
  */
550
598
  getOrCreateFields(path: PathType): Fields;
599
+ /**
600
+ * Removes all child nodes from this tree branch.
601
+ * This method ensures that `destroy()` is called on each child node, allowing for
602
+ * a full, recursive cleanup of the entire subtree.
603
+ */
604
+ clear(): void;
605
+ /**
606
+ * Performs a complete cleanup of this node and its entire subtree.
607
+ *
608
+ * It recursively destroys all child nodes by calling `clear()` and then
609
+ * unsubscribes all listeners from its own event emitters.
610
+ * This method should be called when a node is no longer needed.
611
+ */
612
+ destroy(): void;
551
613
  /**
552
614
  * @private
553
615
  * Navigates the tree to the parent of a target node.
@@ -699,30 +761,19 @@ interface FieldsSnapshot {
699
761
  * into a storable snapshot and back.
700
762
  * It delegates the actual serialization of each `Field` and `Policy` to their respective serializers.
701
763
  *
702
- * @todo This implementation is coupled to creating `DefaultFields` instances during hydration.
703
- * To make the system fully extensible, this class should be refactored to use a
704
- * `FieldsRegistry` (a `ConstructorRegistry<Fields>`). This would allow it to
705
- * hydrate any custom `Fields` class (e.g., `ReactiveFields`) based on the `__type`
706
- * property in the snapshot, mirroring the pattern used by `FieldSerializer`.
707
- *
708
764
  * @todo Implement a `patch(fields, snapshot)` method. It should perform a non-destructive
709
765
  * update, creating new fields, removing missing ones, and patching existing ones
710
766
  * in place, preserving the container instance itself.
711
767
  */
712
- declare class FieldsSerializer {
713
- private readonly fieldRegistry;
714
- private readonly policySerializer;
715
- /**
716
- * An internal instance of FieldSerializer to handle individual fields.
717
- * @private
718
- */
768
+ declare class FieldsSerializer<TFields extends Fields> {
769
+ private readonly fieldsFactory;
719
770
  private readonly fieldSerializer;
720
771
  /**
721
772
  * Creates an instance of FieldsSerializer.
722
- * @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
723
- * @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
773
+ * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
774
+ * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
724
775
  */
725
- constructor(fieldRegistry: FieldRegistry, policySerializer: PolicySerializer);
776
+ constructor(fieldsFactory: FieldsFactory<TFields>, fieldSerializer: FieldSerializer);
726
777
  /**
727
778
  * Creates a serializable snapshot of a `Fields` container.
728
779
  *
@@ -735,12 +786,11 @@ declare class FieldsSerializer {
735
786
  /**
736
787
  * Restores a `Fields` container instance from its snapshot representation.
737
788
  *
738
- * **Limitation:** This method is currently hardcoded to always create an instance of `DefaultFields`.
739
789
  * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
740
790
  * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
741
- * @returns {DefaultFields} A new `DefaultFields` instance populated with the restored fields.
791
+ * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
742
792
  */
743
- hydrate(snapshot: FieldsSnapshot): DefaultFields;
793
+ hydrate(snapshot: FieldsSnapshot): TFields;
744
794
  }
745
795
 
746
796
  /**
@@ -772,21 +822,21 @@ interface FieldTreeSnapshot {
772
822
  * updates. This method should traverse the existing tree and the snapshot,
773
823
  * patching nodes in place to maintain object references.
774
824
  */
775
- declare class FieldTreeSerializer {
825
+ declare class FieldTreeSerializer<TFields extends Fields> {
776
826
  private readonly fieldTreeNodeFactory;
777
827
  private readonly fieldsSerializer;
778
- constructor(fieldTreeNodeFactory: TreeNodeFactory, fieldsSerializer: FieldsSerializer);
828
+ constructor(fieldTreeNodeFactory: TreeNodeFactory<TFields>, fieldsSerializer: FieldsSerializer<TFields>);
779
829
  /**
780
830
  * Creates a serializable snapshot of the entire tree and its contained fields.
781
831
  * @returns A plain JavaScript object representing the complete state managed by this tree.
782
832
  */
783
- snapshot(tree: FieldTree): FieldTreeSnapshot;
833
+ snapshot(tree: FieldTree<TFields>): FieldTreeSnapshot;
784
834
  /**
785
835
  * Restores the state of the tree from a snapshot.
786
836
  * It intelligently creates missing nodes based on `__type` metadata and delegates hydration to child nodes.
787
837
  * @param snapshot The snapshot object to load.
788
838
  */
789
- hydrate(snapshot: FieldTreeSnapshot): FieldTree;
839
+ hydrate(snapshot: FieldTreeSnapshot): FieldTree<TFields>;
790
840
  }
791
841
 
792
- export { type BooleanField, ClampMaxPolicy, ClampMaxPolicySerializerHandler, ClampMinPolicy, ClampMinPolicySerializerHandler, ClampPolicy, ClampPolicySerializerHandler, DefaultBooleanField, type DefaultBooleanFieldOptions, DefaultField, DefaultFields, DefaultNumericField, type DefaultNumericFieldOptions, DefaultStringField, type DefaultStringFieldOptions, DefaultTreeNodeFactory, type Field, type FieldOptions, FieldRegistry, FieldSerializer, type FieldSnapshot, FieldTree, FieldTreeSerializer, type FieldTreeSnapshot, Fields, FieldsSerializer, type FieldsSnapshot, type NumericField, Policies, type Policy, PolicySerializer, type PolicySerializerHandler, type StringField, type TreeNodeFactory, type TreeOrFieldsNode, clampMaxPolicy, clampMinPolicy, clampPolicy };
842
+ export { type BooleanField, ClampMaxPolicy, ClampMaxPolicySerializerHandler, ClampMinPolicy, ClampMinPolicySerializerHandler, ClampPolicy, ClampPolicySerializerHandler, DefaultBooleanField, type DefaultBooleanFieldOptions, DefaultField, DefaultFields, DefaultFieldsFactory, DefaultNumericField, type DefaultNumericFieldOptions, DefaultStringField, type DefaultStringFieldOptions, DefaultTreeNodeFactory, type Field, type FieldOptions, FieldRegistry, FieldSerializer, type FieldSnapshot, FieldTree, FieldTreeSerializer, type FieldTreeSnapshot, Fields, type FieldsFactory, FieldsSerializer, type FieldsSnapshot, type NumericField, Policies, type Policy, PolicySerializer, type PolicySerializerHandler, type StringField, type TreeNode, type TreeNodeFactory, clampMaxPolicy, clampMinPolicy, clampPolicy };
package/dist/index.d.ts CHANGED
@@ -330,6 +330,7 @@ declare class Fields {
330
330
  * Removes all fields from the collection, ensuring each is properly destroyed.
331
331
  */
332
332
  clear(): void;
333
+ destroy(): void;
333
334
  }
334
335
 
335
336
  declare const DefaultFields_base: {
@@ -355,6 +356,7 @@ declare const DefaultFields_base: {
355
356
  get<T extends Field<any>>(name: string): T;
356
357
  remove(names: string | string[]): void;
357
358
  clear(): void;
359
+ destroy(): void;
358
360
  };
359
361
  } & {
360
362
  new (...args: any[]): {
@@ -379,6 +381,7 @@ declare const DefaultFields_base: {
379
381
  get<T extends Field<any>>(name: string): T;
380
382
  remove(names: string | string[]): void;
381
383
  clear(): void;
384
+ destroy(): void;
382
385
  };
383
386
  } & {
384
387
  new (...args: any[]): {
@@ -403,11 +406,13 @@ declare const DefaultFields_base: {
403
406
  get<T extends Field<any>>(name: string): T;
404
407
  remove(names: string | string[]): void;
405
408
  clear(): void;
409
+ destroy(): void;
406
410
  };
407
411
  } & {
408
412
  new (...args: any[]): {
409
- createDefault<T>(name: string, initialValue: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
410
- upsetDefault<T>(name: string, value: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
413
+ createGeneric<T>(name: string, initialValue: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
414
+ upsetGeneric<T>(name: string, value: T, options?: FieldOptions<T> | undefined): DefaultField<T>;
415
+ getGeneric<T>(name: string): DefaultField<T>;
411
416
  readonly typeName: "fields";
412
417
  readonly _fields: Map<string, Field<any>>;
413
418
  readonly _fieldRegistry: FieldRegistry;
@@ -426,31 +431,38 @@ declare const DefaultFields_base: {
426
431
  get<T extends Field<any>>(name: string): T;
427
432
  remove(names: string | string[]): void;
428
433
  clear(): void;
434
+ destroy(): void;
429
435
  };
430
436
  } & typeof Fields;
431
437
  declare class DefaultFields extends DefaultFields_base {
432
438
  }
433
439
 
440
+ interface FieldsFactory<TFields extends Fields> {
441
+ fields(): TFields;
442
+ }
443
+ declare class DefaultFieldsFactory implements FieldsFactory<DefaultFields> {
444
+ private readonly fieldRegistry;
445
+ constructor(fieldRegistry: FieldRegistry);
446
+ fields(): DefaultFields;
447
+ }
434
448
  /**
435
449
  * Defines the contract for a factory that creates nodes for a FieldTree.
436
450
  * This allows for custom implementations of Fields and FieldTree to be used.
437
451
  */
438
- interface TreeNodeFactory {
439
- fields<T extends Fields>(): T;
440
- tree<T extends FieldTree>(): T;
452
+ interface TreeNodeFactory<TFields extends Fields> extends FieldsFactory<TFields> {
453
+ fields(): TFields;
454
+ tree(): FieldTree<TFields>;
441
455
  }
442
456
  /**
443
457
  * The default factory implementation that creates standard DefaultFields and FieldTree instances.
444
458
  */
445
- declare class DefaultTreeNodeFactory implements TreeNodeFactory {
446
- private readonly fieldRegistry;
459
+ declare class DefaultTreeNodeFactory extends DefaultFieldsFactory implements TreeNodeFactory<DefaultFields> {
447
460
  constructor(fieldRegistry: FieldRegistry);
448
- fields: () => any;
449
- tree: () => any;
461
+ tree(): FieldTree<DefaultFields>;
450
462
  }
451
463
 
452
464
  /** A type alias for any container that can be a child node in a FieldTree */
453
- type TreeOrFieldsNode = FieldTree | Fields;
465
+ type TreeNode<F extends Fields> = FieldTree<F> | F;
454
466
  /**
455
467
  * Represents a hierarchical data structure for managing the global state of the system.
456
468
  *
@@ -459,26 +471,51 @@ type TreeOrFieldsNode = FieldTree | Fields;
459
471
  * and overall game progress. It uses a path-based system for accessing and
460
472
  * manipulating nested data, similar to a file system.
461
473
  *
462
- * @todo
463
- * - Add node removal functionality.
464
- * - Implement an event system for node creation/removal.
465
474
  */
466
- declare class FieldTree {
475
+ declare class FieldTree<TFields extends Fields> {
467
476
  static readonly typeName = "fieldTree";
468
477
  readonly typeName = "fieldTree";
469
478
  /** @private The internal map storing child nodes (branches or leaves). */
470
479
  private readonly _nodes;
471
480
  /** @private The factory used to create new child nodes. */
472
481
  private readonly _factory;
482
+ /**
483
+ * An event emitter that fires immediately after a new node is added to this tree branch.
484
+ * @event
485
+ * @param {object} event - The event payload.
486
+ * @param {string} event.name - The name (key) of the added node.
487
+ * @param event.node - The node instance that was added.
488
+ * @example
489
+ * myTree.onAdd.subscribe(({ name, node }) => {
490
+ * console.log(`Node '${name}' was added.`, node);
491
+ * });
492
+ */
493
+ onAdd: Emitter<[event: {
494
+ name: string;
495
+ node: TreeNode<TFields>;
496
+ }]>;
497
+ /**
498
+ * An event emitter that fires once after one or more nodes have been successfully removed.
499
+ * @event
500
+ * @param {object} event - The event payload.
501
+ * @param {string[]} event.names - An array of names of the nodes that were removed.
502
+ * @example
503
+ * myTree.onRemove.subscribe(({ names }) => {
504
+ * console.log(`Nodes removed: ${names.join(', ')}`);
505
+ * });
506
+ */
507
+ onRemove: Emitter<[event: {
508
+ names: string[];
509
+ }]>;
473
510
  /**
474
511
  * Gets the collection of direct child nodes of this tree branch.
475
512
  */
476
- get nodes(): Map<string, TreeOrFieldsNode>;
513
+ get nodes(): Map<string, TreeNode<TFields>>;
477
514
  /**
478
515
  * Creates an instance of FieldTree.
479
516
  * @param {TreeNodeFactory} factory - A factory responsible for creating new nodes within the tree.
480
517
  */
481
- constructor(factory: TreeNodeFactory);
518
+ constructor(factory: TreeNodeFactory<TFields>);
482
519
  /**
483
520
  * Checks if a direct child node with the given name exists.
484
521
  * @param {string} name - The name of the direct child node.
@@ -494,18 +531,29 @@ declare class FieldTree {
494
531
  /**
495
532
  * Adds a pre-existing node as a direct child of this tree branch.
496
533
  * @param {string} name - The name to assign to the new child node.
497
- * @param {TreeOrFieldsNode} node - The node instance to add.
498
- * @returns {TreeOrFieldsNode} The added node.
534
+ * @param {TreeNode} node - The node instance to add.
535
+ * @returns {TreeNode} The added node.
499
536
  * @throws If a node with the same name already exists.
500
537
  */
501
- addNode(name: string, node: TreeOrFieldsNode): TreeOrFieldsNode;
538
+ addNode(name: string, node: TreeNode<TFields>): TreeNode<TFields>;
502
539
  /**
503
540
  * Retrieves a direct child node by its name.
504
541
  * @param {string} name - The name of the child node.
505
- * @returns {TreeOrFieldsNode} The retrieved node.
542
+ * @returns {TreeNode} The retrieved node.
506
543
  * @throws If a node with the given name cannot be found.
507
544
  */
508
- getNode(name: string): TreeOrFieldsNode;
545
+ getNode(name: string): TreeNode<TFields>;
546
+ /**
547
+ * Removes one or more nodes from this tree branch.
548
+ *
549
+ * This method first validates that all specified nodes exist. If validation passes,
550
+ * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
551
+ * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
552
+ *
553
+ * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
554
+ * @throws If any of the specified names do not correspond to an existing node.
555
+ */
556
+ removeNode(names: string | string[]): void;
509
557
  /**
510
558
  * Creates a new `FieldTree` (branch) node at the specified path.
511
559
  * @param {PathType} path - The path where the new `FieldTree` should be created.
@@ -513,7 +561,7 @@ declare class FieldTree {
513
561
  * @returns {FieldTree} The newly created `FieldTree` instance.
514
562
  * @throws If the path is invalid or a node already exists at the target location.
515
563
  */
516
- createFieldTree<T extends FieldTree>(path: PathType, createPath?: boolean): T;
564
+ createFieldTree<T extends FieldTree<TFields>>(path: PathType, createPath?: boolean): T;
517
565
  /**
518
566
  * Creates a new `Fields` (leaf) container at the specified path.
519
567
  * @param {PathType} path - The path where the new `Fields` container should be created.
@@ -521,33 +569,47 @@ declare class FieldTree {
521
569
  * @returns {Fields} The newly created `Fields` instance.
522
570
  * @throws If the path is invalid or a node already exists at the target location.
523
571
  */
524
- createFields<T extends Fields>(path: PathType, createPath?: boolean): T;
572
+ createFields(path: PathType, createPath?: boolean): TFields;
525
573
  /**
526
574
  * Retrieves a `FieldTree` (branch) node from a specified path.
527
575
  * @param {PathType} path - The path to the `FieldTree` node.
528
576
  * @returns {FieldTree} The `FieldTree` instance at the specified path.
529
577
  * @throws If the path is invalid or the node at the path is not a `FieldTree`.
530
578
  */
531
- getFieldTree(path: PathType): FieldTree;
579
+ getFieldTree(path: PathType): FieldTree<TFields>;
532
580
  /**
533
581
  * Retrieves a `Fields` (leaf) container from a specified path.
534
582
  * @param {PathType} path - The path to the `Fields` container.
535
583
  * @returns {Fields} The `Fields` instance at the specified path.
536
584
  * @throws If the path is invalid or the node at the path is not a `Fields` container.
537
585
  */
538
- getFields(path: PathType): Fields;
586
+ getFields(path: PathType): TFields;
539
587
  /**
540
588
  * Retrieves a `FieldTree` at the specified path. If it or any part of the path doesn't exist, it will be created.
541
589
  * @param {PathType} path - The path to the `FieldTree` node.
542
590
  * @returns {FieldTree} The existing or newly created `FieldTree` instance.
543
591
  */
544
- getOrCreateFieldTree(path: PathType): FieldTree;
592
+ getOrCreateFieldTree(path: PathType): FieldTree<TFields>;
545
593
  /**
546
594
  * Retrieves a `Fields` container at the specified path. If it or any part of the path doesn't exist, it will be created.
547
595
  * @param {PathType} path - The path to the `Fields` container.
548
596
  * @returns {Fields} The existing or newly created `Fields` instance.
549
597
  */
550
598
  getOrCreateFields(path: PathType): Fields;
599
+ /**
600
+ * Removes all child nodes from this tree branch.
601
+ * This method ensures that `destroy()` is called on each child node, allowing for
602
+ * a full, recursive cleanup of the entire subtree.
603
+ */
604
+ clear(): void;
605
+ /**
606
+ * Performs a complete cleanup of this node and its entire subtree.
607
+ *
608
+ * It recursively destroys all child nodes by calling `clear()` and then
609
+ * unsubscribes all listeners from its own event emitters.
610
+ * This method should be called when a node is no longer needed.
611
+ */
612
+ destroy(): void;
551
613
  /**
552
614
  * @private
553
615
  * Navigates the tree to the parent of a target node.
@@ -699,30 +761,19 @@ interface FieldsSnapshot {
699
761
  * into a storable snapshot and back.
700
762
  * It delegates the actual serialization of each `Field` and `Policy` to their respective serializers.
701
763
  *
702
- * @todo This implementation is coupled to creating `DefaultFields` instances during hydration.
703
- * To make the system fully extensible, this class should be refactored to use a
704
- * `FieldsRegistry` (a `ConstructorRegistry<Fields>`). This would allow it to
705
- * hydrate any custom `Fields` class (e.g., `ReactiveFields`) based on the `__type`
706
- * property in the snapshot, mirroring the pattern used by `FieldSerializer`.
707
- *
708
764
  * @todo Implement a `patch(fields, snapshot)` method. It should perform a non-destructive
709
765
  * update, creating new fields, removing missing ones, and patching existing ones
710
766
  * in place, preserving the container instance itself.
711
767
  */
712
- declare class FieldsSerializer {
713
- private readonly fieldRegistry;
714
- private readonly policySerializer;
715
- /**
716
- * An internal instance of FieldSerializer to handle individual fields.
717
- * @private
718
- */
768
+ declare class FieldsSerializer<TFields extends Fields> {
769
+ private readonly fieldsFactory;
719
770
  private readonly fieldSerializer;
720
771
  /**
721
772
  * Creates an instance of FieldsSerializer.
722
- * @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
723
- * @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
773
+ * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
774
+ * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
724
775
  */
725
- constructor(fieldRegistry: FieldRegistry, policySerializer: PolicySerializer);
776
+ constructor(fieldsFactory: FieldsFactory<TFields>, fieldSerializer: FieldSerializer);
726
777
  /**
727
778
  * Creates a serializable snapshot of a `Fields` container.
728
779
  *
@@ -735,12 +786,11 @@ declare class FieldsSerializer {
735
786
  /**
736
787
  * Restores a `Fields` container instance from its snapshot representation.
737
788
  *
738
- * **Limitation:** This method is currently hardcoded to always create an instance of `DefaultFields`.
739
789
  * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
740
790
  * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
741
- * @returns {DefaultFields} A new `DefaultFields` instance populated with the restored fields.
791
+ * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
742
792
  */
743
- hydrate(snapshot: FieldsSnapshot): DefaultFields;
793
+ hydrate(snapshot: FieldsSnapshot): TFields;
744
794
  }
745
795
 
746
796
  /**
@@ -772,21 +822,21 @@ interface FieldTreeSnapshot {
772
822
  * updates. This method should traverse the existing tree and the snapshot,
773
823
  * patching nodes in place to maintain object references.
774
824
  */
775
- declare class FieldTreeSerializer {
825
+ declare class FieldTreeSerializer<TFields extends Fields> {
776
826
  private readonly fieldTreeNodeFactory;
777
827
  private readonly fieldsSerializer;
778
- constructor(fieldTreeNodeFactory: TreeNodeFactory, fieldsSerializer: FieldsSerializer);
828
+ constructor(fieldTreeNodeFactory: TreeNodeFactory<TFields>, fieldsSerializer: FieldsSerializer<TFields>);
779
829
  /**
780
830
  * Creates a serializable snapshot of the entire tree and its contained fields.
781
831
  * @returns A plain JavaScript object representing the complete state managed by this tree.
782
832
  */
783
- snapshot(tree: FieldTree): FieldTreeSnapshot;
833
+ snapshot(tree: FieldTree<TFields>): FieldTreeSnapshot;
784
834
  /**
785
835
  * Restores the state of the tree from a snapshot.
786
836
  * It intelligently creates missing nodes based on `__type` metadata and delegates hydration to child nodes.
787
837
  * @param snapshot The snapshot object to load.
788
838
  */
789
- hydrate(snapshot: FieldTreeSnapshot): FieldTree;
839
+ hydrate(snapshot: FieldTreeSnapshot): FieldTree<TFields>;
790
840
  }
791
841
 
792
- export { type BooleanField, ClampMaxPolicy, ClampMaxPolicySerializerHandler, ClampMinPolicy, ClampMinPolicySerializerHandler, ClampPolicy, ClampPolicySerializerHandler, DefaultBooleanField, type DefaultBooleanFieldOptions, DefaultField, DefaultFields, DefaultNumericField, type DefaultNumericFieldOptions, DefaultStringField, type DefaultStringFieldOptions, DefaultTreeNodeFactory, type Field, type FieldOptions, FieldRegistry, FieldSerializer, type FieldSnapshot, FieldTree, FieldTreeSerializer, type FieldTreeSnapshot, Fields, FieldsSerializer, type FieldsSnapshot, type NumericField, Policies, type Policy, PolicySerializer, type PolicySerializerHandler, type StringField, type TreeNodeFactory, type TreeOrFieldsNode, clampMaxPolicy, clampMinPolicy, clampPolicy };
842
+ export { type BooleanField, ClampMaxPolicy, ClampMaxPolicySerializerHandler, ClampMinPolicy, ClampMinPolicySerializerHandler, ClampPolicy, ClampPolicySerializerHandler, DefaultBooleanField, type DefaultBooleanFieldOptions, DefaultField, DefaultFields, DefaultFieldsFactory, DefaultNumericField, type DefaultNumericFieldOptions, DefaultStringField, type DefaultStringFieldOptions, DefaultTreeNodeFactory, type Field, type FieldOptions, FieldRegistry, FieldSerializer, type FieldSnapshot, FieldTree, FieldTreeSerializer, type FieldTreeSnapshot, Fields, type FieldsFactory, FieldsSerializer, type FieldsSnapshot, type NumericField, Policies, type Policy, PolicySerializer, type PolicySerializerHandler, type StringField, type TreeNode, type TreeNodeFactory, clampMaxPolicy, clampMinPolicy, clampPolicy };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  DefaultBooleanField: () => DefaultBooleanField,
30
30
  DefaultField: () => DefaultField,
31
31
  DefaultFields: () => DefaultFields,
32
+ DefaultFieldsFactory: () => DefaultFieldsFactory,
32
33
  DefaultNumericField: () => DefaultNumericField,
33
34
  DefaultStringField: () => DefaultStringField,
34
35
  DefaultTreeNodeFactory: () => DefaultTreeNodeFactory,
@@ -491,6 +492,11 @@ var Fields = class _Fields {
491
492
  clear() {
492
493
  this.remove(Array.from(this._fields.keys()));
493
494
  }
495
+ destroy() {
496
+ this.clear();
497
+ this.onAdd.clear();
498
+ this.onRemove.clear();
499
+ }
494
500
  };
495
501
 
496
502
  // src/mixins/with-boolean-fields.mixin.ts
@@ -538,20 +544,23 @@ function WithNumericFields(Base) {
538
544
  };
539
545
  }
540
546
 
541
- // src/mixins/with-default-fields.mixin.ts
542
- function WithDefaultFields(Base) {
543
- return class FieldsWithDefault extends Base {
544
- createDefault(name, initialValue, options) {
547
+ // src/mixins/with-default-generic-fields.mixin.ts
548
+ function WithDefaultGenericFields(Base) {
549
+ return class FieldsWithDefaultGeneric extends Base {
550
+ createGeneric(name, initialValue, options) {
545
551
  return this.create(DefaultField.typeName, name, initialValue, options);
546
552
  }
547
- upsetDefault(name, value, options) {
553
+ upsetGeneric(name, value, options) {
548
554
  return this.upset(DefaultField.typeName, name, value, options);
549
555
  }
556
+ getGeneric(name) {
557
+ return this.get(name);
558
+ }
550
559
  };
551
560
  }
552
561
 
553
562
  // src/default-fields.ts
554
- var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultFields(Fields)))) {
563
+ var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultGenericFields(Fields)))) {
555
564
  };
556
565
 
557
566
  // src/field-tree.ts
@@ -563,6 +572,29 @@ var FieldTree = class _FieldTree {
563
572
  _nodes = /* @__PURE__ */ new Map();
564
573
  /** @private The factory used to create new child nodes. */
565
574
  _factory;
575
+ /**
576
+ * An event emitter that fires immediately after a new node is added to this tree branch.
577
+ * @event
578
+ * @param {object} event - The event payload.
579
+ * @param {string} event.name - The name (key) of the added node.
580
+ * @param event.node - The node instance that was added.
581
+ * @example
582
+ * myTree.onAdd.subscribe(({ name, node }) => {
583
+ * console.log(`Node '${name}' was added.`, node);
584
+ * });
585
+ */
586
+ onAdd = new import_utils5.Emitter();
587
+ /**
588
+ * An event emitter that fires once after one or more nodes have been successfully removed.
589
+ * @event
590
+ * @param {object} event - The event payload.
591
+ * @param {string[]} event.names - An array of names of the nodes that were removed.
592
+ * @example
593
+ * myTree.onRemove.subscribe(({ names }) => {
594
+ * console.log(`Nodes removed: ${names.join(', ')}`);
595
+ * });
596
+ */
597
+ onRemove = new import_utils5.Emitter();
566
598
  /**
567
599
  * Gets the collection of direct child nodes of this tree branch.
568
600
  */
@@ -596,19 +628,20 @@ var FieldTree = class _FieldTree {
596
628
  /**
597
629
  * Adds a pre-existing node as a direct child of this tree branch.
598
630
  * @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.
631
+ * @param {TreeNode} node - The node instance to add.
632
+ * @returns {TreeNode} The added node.
601
633
  * @throws If a node with the same name already exists.
602
634
  */
603
635
  addNode(name, node) {
604
636
  (0, import_utils5.throwIf)(this.has(name), `Can't add node with name: '${name}', node already exists`);
605
637
  this._nodes.set(name, node);
638
+ this.onAdd.emit({ name, node });
606
639
  return node;
607
640
  }
608
641
  /**
609
642
  * Retrieves a direct child node by its name.
610
643
  * @param {string} name - The name of the child node.
611
- * @returns {TreeOrFieldsNode} The retrieved node.
644
+ * @returns {TreeNode} The retrieved node.
612
645
  * @throws If a node with the given name cannot be found.
613
646
  */
614
647
  getNode(name) {
@@ -616,6 +649,29 @@ var FieldTree = class _FieldTree {
616
649
  (0, import_utils5.throwIfEmpty)(node, `Can't find node with name '${name}'`);
617
650
  return node;
618
651
  }
652
+ /**
653
+ * Removes one or more nodes from this tree branch.
654
+ *
655
+ * This method first validates that all specified nodes exist. If validation passes,
656
+ * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
657
+ * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
658
+ *
659
+ * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
660
+ * @throws If any of the specified names do not correspond to an existing node.
661
+ */
662
+ removeNode(names) {
663
+ const toRemoveNames = Array.isArray(names) ? names : [names];
664
+ toRemoveNames.forEach((name) => {
665
+ (0, import_utils5.throwIf)(!this.has(name), `Can't remove node with name: '${name}', node doesn't exists`);
666
+ });
667
+ toRemoveNames.forEach((name) => {
668
+ this._nodes.get(name).destroy();
669
+ this._nodes.delete(name);
670
+ });
671
+ if (toRemoveNames.length) {
672
+ this.onRemove.emit({ names: toRemoveNames });
673
+ }
674
+ }
619
675
  /**
620
676
  * Creates a new `FieldTree` (branch) node at the specified path.
621
677
  * @param {PathType} path - The path where the new `FieldTree` should be created.
@@ -686,6 +742,26 @@ var FieldTree = class _FieldTree {
686
742
  const traversedPath = this.traversePath(path, true);
687
743
  return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
688
744
  }
745
+ /**
746
+ * Removes all child nodes from this tree branch.
747
+ * This method ensures that `destroy()` is called on each child node, allowing for
748
+ * a full, recursive cleanup of the entire subtree.
749
+ */
750
+ clear() {
751
+ this.removeNode(Array.from(this._nodes.keys()));
752
+ }
753
+ /**
754
+ * Performs a complete cleanup of this node and its entire subtree.
755
+ *
756
+ * It recursively destroys all child nodes by calling `clear()` and then
757
+ * unsubscribes all listeners from its own event emitters.
758
+ * This method should be called when a node is no longer needed.
759
+ */
760
+ destroy() {
761
+ this.clear();
762
+ this.onAdd.clear();
763
+ this.onRemove.clear();
764
+ }
689
765
  /**
690
766
  * @private
691
767
  * Navigates the tree to the parent of a target node.
@@ -718,12 +794,21 @@ var FieldTree = class _FieldTree {
718
794
  };
719
795
 
720
796
  // src/field-tree-node-factory.ts
721
- var DefaultTreeNodeFactory = class {
797
+ var DefaultFieldsFactory = class {
722
798
  constructor(fieldRegistry) {
723
799
  this.fieldRegistry = fieldRegistry;
724
800
  }
725
- fields = () => new DefaultFields(this.fieldRegistry);
726
- tree = () => new FieldTree(this);
801
+ fields() {
802
+ return new DefaultFields(this.fieldRegistry);
803
+ }
804
+ };
805
+ var DefaultTreeNodeFactory = class extends DefaultFieldsFactory {
806
+ constructor(fieldRegistry) {
807
+ super(fieldRegistry);
808
+ }
809
+ tree() {
810
+ return new FieldTree(this);
811
+ }
727
812
  };
728
813
 
729
814
  // src/serializer/policies/clamp-policy-serializer-handler.ts
@@ -856,19 +941,13 @@ var FieldSerializer = class {
856
941
  var FieldsSerializer = class {
857
942
  /**
858
943
  * 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.
944
+ * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
945
+ * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
861
946
  */
862
- constructor(fieldRegistry, policySerializer) {
863
- this.fieldRegistry = fieldRegistry;
864
- this.policySerializer = policySerializer;
865
- this.fieldSerializer = new FieldSerializer(this.fieldRegistry, this.policySerializer);
947
+ constructor(fieldsFactory, fieldSerializer) {
948
+ this.fieldsFactory = fieldsFactory;
949
+ this.fieldSerializer = fieldSerializer;
866
950
  }
867
- /**
868
- * An internal instance of FieldSerializer to handle individual fields.
869
- * @private
870
- */
871
- fieldSerializer;
872
951
  /**
873
952
  * Creates a serializable snapshot of a `Fields` container.
874
953
  *
@@ -887,14 +966,13 @@ var FieldsSerializer = class {
887
966
  /**
888
967
  * Restores a `Fields` container instance from its snapshot representation.
889
968
  *
890
- * **Limitation:** This method is currently hardcoded to always create an instance of `DefaultFields`.
891
969
  * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
892
970
  * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
893
- * @returns {DefaultFields} A new `DefaultFields` instance populated with the restored fields.
971
+ * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
894
972
  */
895
973
  hydrate(snapshot) {
896
974
  const { __type, ...fieldsData } = snapshot;
897
- const fields = new DefaultFields(this.fieldRegistry);
975
+ const fields = this.fieldsFactory.fields();
898
976
  for (const fieldName in fieldsData) {
899
977
  const fieldSnapshot = fieldsData[fieldName];
900
978
  const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
@@ -943,7 +1021,7 @@ var FieldTreeSerializer = class {
943
1021
  }
944
1022
  if (nodeData.__type === FieldTree.typeName) {
945
1023
  tree.addNode(key, this.hydrate(nodeData));
946
- } else {
1024
+ } else if (nodeData.__type === Fields.typeName) {
947
1025
  tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
948
1026
  }
949
1027
  }
@@ -961,6 +1039,7 @@ var FieldTreeSerializer = class {
961
1039
  DefaultBooleanField,
962
1040
  DefaultField,
963
1041
  DefaultFields,
1042
+ DefaultFieldsFactory,
964
1043
  DefaultNumericField,
965
1044
  DefaultStringField,
966
1045
  DefaultTreeNodeFactory,
package/dist/index.mjs CHANGED
@@ -443,6 +443,11 @@ var Fields = class _Fields {
443
443
  clear() {
444
444
  this.remove(Array.from(this._fields.keys()));
445
445
  }
446
+ destroy() {
447
+ this.clear();
448
+ this.onAdd.clear();
449
+ this.onRemove.clear();
450
+ }
446
451
  };
447
452
 
448
453
  // src/mixins/with-boolean-fields.mixin.ts
@@ -490,24 +495,27 @@ function WithNumericFields(Base) {
490
495
  };
491
496
  }
492
497
 
493
- // src/mixins/with-default-fields.mixin.ts
494
- function WithDefaultFields(Base) {
495
- return class FieldsWithDefault extends Base {
496
- createDefault(name, initialValue, options) {
498
+ // src/mixins/with-default-generic-fields.mixin.ts
499
+ function WithDefaultGenericFields(Base) {
500
+ return class FieldsWithDefaultGeneric extends Base {
501
+ createGeneric(name, initialValue, options) {
497
502
  return this.create(DefaultField.typeName, name, initialValue, options);
498
503
  }
499
- upsetDefault(name, value, options) {
504
+ upsetGeneric(name, value, options) {
500
505
  return this.upset(DefaultField.typeName, name, value, options);
501
506
  }
507
+ getGeneric(name) {
508
+ return this.get(name);
509
+ }
502
510
  };
503
511
  }
504
512
 
505
513
  // src/default-fields.ts
506
- var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultFields(Fields)))) {
514
+ var DefaultFields = class extends WithBooleanFields(WithStringFields(WithNumericFields(WithDefaultGenericFields(Fields)))) {
507
515
  };
508
516
 
509
517
  // src/field-tree.ts
510
- import { ensurePathArray, ensurePathString, throwIf as throwIf3, throwIfEmpty as throwIfEmpty2 } from "@axi-engine/utils";
518
+ import { Emitter as Emitter3, ensurePathArray, ensurePathString, throwIf as throwIf3, throwIfEmpty as throwIfEmpty2 } from "@axi-engine/utils";
511
519
  var FieldTree = class _FieldTree {
512
520
  static typeName = "fieldTree";
513
521
  typeName = _FieldTree.typeName;
@@ -515,6 +523,29 @@ var FieldTree = class _FieldTree {
515
523
  _nodes = /* @__PURE__ */ new Map();
516
524
  /** @private The factory used to create new child nodes. */
517
525
  _factory;
526
+ /**
527
+ * An event emitter that fires immediately after a new node is added to this tree branch.
528
+ * @event
529
+ * @param {object} event - The event payload.
530
+ * @param {string} event.name - The name (key) of the added node.
531
+ * @param event.node - The node instance that was added.
532
+ * @example
533
+ * myTree.onAdd.subscribe(({ name, node }) => {
534
+ * console.log(`Node '${name}' was added.`, node);
535
+ * });
536
+ */
537
+ onAdd = new Emitter3();
538
+ /**
539
+ * An event emitter that fires once after one or more nodes have been successfully removed.
540
+ * @event
541
+ * @param {object} event - The event payload.
542
+ * @param {string[]} event.names - An array of names of the nodes that were removed.
543
+ * @example
544
+ * myTree.onRemove.subscribe(({ names }) => {
545
+ * console.log(`Nodes removed: ${names.join(', ')}`);
546
+ * });
547
+ */
548
+ onRemove = new Emitter3();
518
549
  /**
519
550
  * Gets the collection of direct child nodes of this tree branch.
520
551
  */
@@ -548,19 +579,20 @@ var FieldTree = class _FieldTree {
548
579
  /**
549
580
  * Adds a pre-existing node as a direct child of this tree branch.
550
581
  * @param {string} name - The name to assign to the new child node.
551
- * @param {TreeOrFieldsNode} node - The node instance to add.
552
- * @returns {TreeOrFieldsNode} The added node.
582
+ * @param {TreeNode} node - The node instance to add.
583
+ * @returns {TreeNode} The added node.
553
584
  * @throws If a node with the same name already exists.
554
585
  */
555
586
  addNode(name, node) {
556
587
  throwIf3(this.has(name), `Can't add node with name: '${name}', node already exists`);
557
588
  this._nodes.set(name, node);
589
+ this.onAdd.emit({ name, node });
558
590
  return node;
559
591
  }
560
592
  /**
561
593
  * Retrieves a direct child node by its name.
562
594
  * @param {string} name - The name of the child node.
563
- * @returns {TreeOrFieldsNode} The retrieved node.
595
+ * @returns {TreeNode} The retrieved node.
564
596
  * @throws If a node with the given name cannot be found.
565
597
  */
566
598
  getNode(name) {
@@ -568,6 +600,29 @@ var FieldTree = class _FieldTree {
568
600
  throwIfEmpty2(node, `Can't find node with name '${name}'`);
569
601
  return node;
570
602
  }
603
+ /**
604
+ * Removes one or more nodes from this tree branch.
605
+ *
606
+ * This method first validates that all specified nodes exist. If validation passes,
607
+ * it recursively calls `destroy()` on each node to ensure proper cleanup of the entire subtree.
608
+ * Finally, it emits a single `onRemove` event with the names of all successfully removed nodes.
609
+ *
610
+ * @param {string | string[]} names - A single name or an array of names of the nodes to remove.
611
+ * @throws If any of the specified names do not correspond to an existing node.
612
+ */
613
+ removeNode(names) {
614
+ const toRemoveNames = Array.isArray(names) ? names : [names];
615
+ toRemoveNames.forEach((name) => {
616
+ throwIf3(!this.has(name), `Can't remove node with name: '${name}', node doesn't exists`);
617
+ });
618
+ toRemoveNames.forEach((name) => {
619
+ this._nodes.get(name).destroy();
620
+ this._nodes.delete(name);
621
+ });
622
+ if (toRemoveNames.length) {
623
+ this.onRemove.emit({ names: toRemoveNames });
624
+ }
625
+ }
571
626
  /**
572
627
  * Creates a new `FieldTree` (branch) node at the specified path.
573
628
  * @param {PathType} path - The path where the new `FieldTree` should be created.
@@ -638,6 +693,26 @@ var FieldTree = class _FieldTree {
638
693
  const traversedPath = this.traversePath(path, true);
639
694
  return traversedPath.branch.has(traversedPath.leafName) ? traversedPath.branch.getFields(traversedPath.leafName) : traversedPath.branch.createFields(traversedPath.leafName);
640
695
  }
696
+ /**
697
+ * Removes all child nodes from this tree branch.
698
+ * This method ensures that `destroy()` is called on each child node, allowing for
699
+ * a full, recursive cleanup of the entire subtree.
700
+ */
701
+ clear() {
702
+ this.removeNode(Array.from(this._nodes.keys()));
703
+ }
704
+ /**
705
+ * Performs a complete cleanup of this node and its entire subtree.
706
+ *
707
+ * It recursively destroys all child nodes by calling `clear()` and then
708
+ * unsubscribes all listeners from its own event emitters.
709
+ * This method should be called when a node is no longer needed.
710
+ */
711
+ destroy() {
712
+ this.clear();
713
+ this.onAdd.clear();
714
+ this.onRemove.clear();
715
+ }
641
716
  /**
642
717
  * @private
643
718
  * Navigates the tree to the parent of a target node.
@@ -670,12 +745,21 @@ var FieldTree = class _FieldTree {
670
745
  };
671
746
 
672
747
  // src/field-tree-node-factory.ts
673
- var DefaultTreeNodeFactory = class {
748
+ var DefaultFieldsFactory = class {
674
749
  constructor(fieldRegistry) {
675
750
  this.fieldRegistry = fieldRegistry;
676
751
  }
677
- fields = () => new DefaultFields(this.fieldRegistry);
678
- tree = () => new FieldTree(this);
752
+ fields() {
753
+ return new DefaultFields(this.fieldRegistry);
754
+ }
755
+ };
756
+ var DefaultTreeNodeFactory = class extends DefaultFieldsFactory {
757
+ constructor(fieldRegistry) {
758
+ super(fieldRegistry);
759
+ }
760
+ tree() {
761
+ return new FieldTree(this);
762
+ }
679
763
  };
680
764
 
681
765
  // src/serializer/policies/clamp-policy-serializer-handler.ts
@@ -808,19 +892,13 @@ var FieldSerializer = class {
808
892
  var FieldsSerializer = class {
809
893
  /**
810
894
  * Creates an instance of FieldsSerializer.
811
- * @param {FieldRegistry} fieldRegistry - A registry that maps string type names to Field constructors.
812
- * @param {PolicySerializer} policySerializer - A serializer dedicated to handling Policy instances.
895
+ * @param {FieldsFactory} fieldsFactory - A registry that maps string type names to Field constructors.
896
+ * @param {FieldSerializer} fieldSerializer - A serializer of field instances.
813
897
  */
814
- constructor(fieldRegistry, policySerializer) {
815
- this.fieldRegistry = fieldRegistry;
816
- this.policySerializer = policySerializer;
817
- this.fieldSerializer = new FieldSerializer(this.fieldRegistry, this.policySerializer);
898
+ constructor(fieldsFactory, fieldSerializer) {
899
+ this.fieldsFactory = fieldsFactory;
900
+ this.fieldSerializer = fieldSerializer;
818
901
  }
819
- /**
820
- * An internal instance of FieldSerializer to handle individual fields.
821
- * @private
822
- */
823
- fieldSerializer;
824
902
  /**
825
903
  * Creates a serializable snapshot of a `Fields` container.
826
904
  *
@@ -839,14 +917,13 @@ var FieldsSerializer = class {
839
917
  /**
840
918
  * Restores a `Fields` container instance from its snapshot representation.
841
919
  *
842
- * **Limitation:** This method is currently hardcoded to always create an instance of `DefaultFields`.
843
920
  * It iterates through the field snapshots and hydrates them individually, adding them to the new container.
844
921
  * @param {FieldsSnapshot} snapshot - The plain object snapshot to deserialize.
845
- * @returns {DefaultFields} A new `DefaultFields` instance populated with the restored fields.
922
+ * @returns {Fields} A new `DefaultFields` instance populated with the restored fields.
846
923
  */
847
924
  hydrate(snapshot) {
848
925
  const { __type, ...fieldsData } = snapshot;
849
- const fields = new DefaultFields(this.fieldRegistry);
926
+ const fields = this.fieldsFactory.fields();
850
927
  for (const fieldName in fieldsData) {
851
928
  const fieldSnapshot = fieldsData[fieldName];
852
929
  const restoredField = this.fieldSerializer.hydrate(fieldSnapshot);
@@ -895,7 +972,7 @@ var FieldTreeSerializer = class {
895
972
  }
896
973
  if (nodeData.__type === FieldTree.typeName) {
897
974
  tree.addNode(key, this.hydrate(nodeData));
898
- } else {
975
+ } else if (nodeData.__type === Fields.typeName) {
899
976
  tree.addNode(key, this.fieldsSerializer.hydrate(nodeData));
900
977
  }
901
978
  }
@@ -912,6 +989,7 @@ export {
912
989
  DefaultBooleanField,
913
990
  DefaultField,
914
991
  DefaultFields,
992
+ DefaultFieldsFactory,
915
993
  DefaultNumericField,
916
994
  DefaultStringField,
917
995
  DefaultTreeNodeFactory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axi-engine/fields",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "keywords": [