UNPKG

@bunbox/tree

Version:

Tree data structure implementation for Bunbox

137 lines (136 loc) 5.25 kB
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 {};