@bunbox/tree
Version:
Tree data structure implementation for Bunbox
137 lines (136 loc) • 5.25 kB
TypeScript
import { EventEmitter, type EventMap, type MergeEventMaps } from '@bunbox/utils';
type NodeEvents = {
'add-child': [child: AbstractNode];
'remove-child': [child: AbstractNode];
rename: [child: AbstractNode, prev: string, next: string];
'enabled-change': [child: AbstractNode];
};
/**
* Base class for all nodes in the tree.
*
* Responsibilities:
* - Identity (unique `id`)
* - Parent/children management (acyclic, re-parenting supported)
* - Dirty-state tracking on property and structural changes
* - Name/type indexing for fast lookup (global maps)
* - Enabled state (skip traversal by default when disabled)
* - Event propagation: child events are re-emitted by ancestors
*
* Generics:
* - `P`: Shape of the reactive `properties` bag. Assignments mark the node as dirty.
* - `M`: Shape of the `metadata` bag (free-form, not reactive by default).
* - `T`: Additional event map merged with built-in `NodeEvents`.
*/
export declare abstract class AbstractNode<P extends Record<string, any> = Record<string, any>, M extends Record<string, any> = Record<string, any>, T extends EventMap = {}> extends EventEmitter<MergeEventMaps<NodeEvents, T>> {
#private;
metadata: Partial<M>;
/**
* Create a new node.
* @param name Optional initial name. When provided, the node is indexed by this name.
*/
constructor(name?: string);
/**
* Unique identifier of this node. Generated using ULID.
*/
get id(): string;
/**
* Reactive properties bag (shape `P`).
* - Assigning or deleting a property marks the node as dirty.
* - Use for gameplay/state properties that should trigger updates.
*/
get properties(): P;
/**
* The parent node, or `null` if this node is the root of its tree.
*/
get parent(): AbstractNode<Record<string, any>, Record<string, any>, {}> | null;
/**
* Read-only snapshot of direct children.
*/
get children(): readonly AbstractNode[];
/**
* Node name used for indexing and lookups via `findByName`.
*/
get name(): string;
/**
* Change the node name. Updates the name index and emits `rename`.
*/
set name(value: string);
/**
* Whether this node is enabled. Disabled nodes are ignored by traversal by default.
*/
get isEnabled(): boolean;
/** Enable this node. */
enable(): this;
/** Disable this node. */
disable(): this;
/**
* Set the enabled state and emit `enabled-change` when it changes.
*
* Note: By design, the `enabled-change` event payload includes only the node itself.
*/
setEnabled(value: boolean): this;
/**
* Add a child to this node.
* - Throws if attempting to add itself or create a cycle (ancestor as child).
* - If the child had a previous parent, it will be re-parented.
* - Emits `add-child` on this node and re-emits child events up the chain.
* - Invokes the child's protected `_ready()` hook after binding.
* @returns `true` when the child is attached or already present.
*/
addChild(child: AbstractNode): boolean;
/**
* Remove a child from this node.
* - Unbinds event propagation from the removed child.
* - Emits `remove-child`.
* @returns `false` if the child was not attached, otherwise `true`.
*/
removeChild(child: AbstractNode): boolean;
getById(id: string): AbstractNode | null;
/**
* Find nodes by name across all existing nodes.
* @param name The name to look up.
* @returns An array of nodes currently indexed by the given name.
*/
findByName(name: string): AbstractNode[];
/**
* Find nodes by constructor type across all existing nodes.
* @param type The class constructor to search for (uses `type.name`).
* @returns An array of nodes matching the type.
*/
findByType<T extends AbstractNode>(type: new (...args: any[]) => T): T[];
/**
* Get the root node of this node's tree (walks parents to the top).
*/
getRoot(): AbstractNode;
/**
* Traverse the tree from this node and invoke `visitor` for each visited node.
*
* Behavior:
* - By default, disabled nodes (isEnabled === false) are skipped along with their subtrees.
* - Set `includeDisabled` to `true` to include disabled nodes in traversal.
* - Use `order: 'pre' | 'post'` to control visit order (default: `'pre'`).
*
* @param visitor Function invoked for each visited node.
* @param options Optional traversal options.
*/
traverse(visitor: (node: AbstractNode) => void, options?: {
includeDisabled?: boolean;
order?: 'pre' | 'post';
}): void;
protected _ready(): void;
/** @internal */
protected abstract _getType(): string;
}
/**
* Default concrete node type when you don't need a custom subclass.
*
* Example:
* const root = new Node();
* const child = new Node('child');
* root.addChild(child);
*/
export declare class Node<P extends Record<string, any> = Record<string, any>, M extends Record<string, any> = Record<string, any>, T extends EventMap = {}> extends AbstractNode<P, M, T> {
/** @internal */
protected _getType(): string;
}
export {};