UNPKG

@bunbox/tree

Version:

Tree data structure implementation for Bunbox

204 lines (145 loc) 5.4 kB
# @bunbox/tree Lightweight, event-driven, TypeScript-first tree structure used in the Bunbox ecosystem. It provides a base `AbstractNode` with unique identity, acyclic parent/child management with safe reparenting, reactive dirty-state, name/type indexing for fast lookups, and a configurable traversal that skips disabled nodes by default. - Acyclic with safe reparenting - Event chaining (child events are re-emitted by ancestors) - Reactive properties mark the node as dirty automatically - Global lookup by `id`, `name`, and `type` - `isEnabled` state to toggle nodes and their subtrees - `traverse()` with `pre`/`post` order and option to include disabled nodes ## Installation ```sh # npm npm install @bunbox/tree # bun bun add @bunbox/tree ``` Requirements: - Node >= 20 or Bun >= 1.2 - TypeScript >= 5 (recommended) ## Concepts - `AbstractNode<P, M, T>`: generic base class. - `P`: shape of the reactive `properties` bag (mutations mark the node as dirty). - `M`: shape of `metadata` (free-form, non-reactive by default). - `T`: additional events merged into the built-in node events. - `Node`: default concrete implementation when you don’t need a custom subclass. ## Core API (at a glance) Properties and getters: - `id: string` — node ULID - `name: string` — indexed name (used by `findByName`) - `parent: AbstractNode | null` — node parent - `children: readonly AbstractNode[]` — read-only snapshot of direct children - `properties: P` — reactive bag (set/delete marks dirty) - `metadata: Partial<M>` — free-form metadata (non-reactive by default) - `isEnabled: boolean` — enablement state Structural methods: - `addChild(child)` — attach child (reparent if needed) - `removeChild(child)` — detach child - `getRoot()` — walk up to the root Lookup/indexing: - `getById(id)` — direct lookup by id - `findByName(name)` — all nodes currently indexed by that name - `findByType(Ctor)` — all nodes whose constructor has `Ctor.name` Enablement: - `enable()` / `disable()` / `setEnabled(boolean)` — toggles `isEnabled` and emits `enabled-change` Traversal: - `traverse(visitor, options?)` — walk the tree - `options.includeDisabled?: boolean` (default: `false`) - `options.order?: 'pre' | 'post'` (default: `'pre'`) - By default, disabled nodes and their subtrees are skipped Events: - `'add-child'`: [child] - `'remove-child'`: [child] - `'rename'`: [node, prevName, nextName] - `'enabled-change'`: [node] Note: events emitted by children are re-emitted by ancestors, so you can listen at the top of the tree if desired. Full event API provided by `EventEmitter` (`on`, `off`, `once`, `emit`, `subscribe`, etc.). ## Examples ### Creating nodes and structure ```ts import { Node } from '@bunbox/tree'; const root = new Node('root'); const player = new Node('Player'); const enemy = new Node('Enemy'); root.addChild(player); enemy.name = 'EnemyBoss'; root.addChild(enemy); ``` ### Reactive properties and dirty-state ```ts // Define properties shape via generics if you want type Props = { hp: number; speed?: number }; class Character extends Node<Props> {} const c = new Character('char'); c.properties.hp = 100; // marks dirty delete c.properties.speed; // marks dirty ``` ### Events ```ts root.subscribe('add-child', (child) => { console.log('Child added:', child.name); }); root.subscribe('rename', (node, prev, next) => { console.log(`Renamed ${prev} -> ${next}`); }); root.subscribe('enabled-change', (node) => { console.log(`Enabled changed for: ${node.name}`); }); ``` ### Enable/Disable and Traverse ```ts // Disable a node to skip it in default traversal enemy.disable(); // Pre-order (default), skips disabled by default root.traverse((node) => { console.log('visit:', node.name); }); // Include disabled nodes root.traverse( (node) => { console.log('visit including disabled:', node.name); }, { includeDisabled: true }, ); // Post-order root.traverse( (node) => { console.log('post visit:', node.name); }, { order: 'post' }, ); ``` ### Lookups ```ts // By name const enemies = root.findByName('EnemyBoss'); // By type (constructor) class EnemyNode extends Node {} const e1 = new EnemyNode('E1'); root.addChild(e1); const found = root.findByType(EnemyNode); // [e1] ``` ### Safe reparenting ```ts const arm = new Node('Arm'); player.addChild(arm); // Move `arm` under `enemy` (reparenting) enemy.addChild(arm); // detaches from `player` and attaches to `enemy` ``` ## Design Notes - The structure is acyclic: you cannot create cycles (an ancestor cannot be added as a child). - `properties` is a reactive `Proxy`: set/delete marks the node as dirty. - `metadata` is free-form (does not mark dirty by default) — useful for auxiliary data. - The `'enabled-change'` event sends only the node as payload. - Global indexing by `id`, `name`, and `type` accelerates lookups. Indices are automatically updated on rename/attach/detach/dispose. ## TypeScript The API is fully typed and supports specialization via generics: ```ts class MyNode extends Node<{ health: number }, { team?: string }> {} const n = new MyNode('hero'); n.properties.health = 50; // typed n.metadata.team = 'blue'; // typed ``` ## Build/Dev - `bun run dev` — Type checking in watch mode - `bun run build` — Bundle + types in `dist/` ## License MIT © Vulppi — see [LICENSE](/LICENSE.md)