/** * A node in a tree structure. */ export abstract class Node { /** * Returns the ID of this node. */ abstract identity(): string; /** * Attach this node as a child of the given parent node. * One node can only have one parent. * @param parent another node */ abstract attach(parent: this): void; /** * Detach this node from its current parent. * @throws Error if no parent. */ abstract detach(): void; /** * Append a child node to this node. * @param child */ abstract appendChild(child: this): void; /** * Remove a child node from this node. * Do nothing if the given node is not a child. * @param child */ abstract removeChild(child: this): void; /** * Return the parent node of this projection node. */ abstract parent(): this | null; /** * Return the child nodes of this projection node. */ abstract children(): ReadonlySet; /** * Dispose this node and all its children. * Must be invoked when the node is no longer needed. * Duplicate invocations have no effect. */ abstract dispose(): void; /** * Traverse down the node tree starting from this node, inclusive, depth-first. * * @param consumer invoked on each node, including this node, * where a `false` return value will stop the traversal of children. * Note that returning false will not stop the traversal of siblings. * * @example * Since the consumer is always invoked on the current node first, * the following code allows you to get the actual node instance * from a proxy. * ```ts * let actual!: Node; * proxy.traverse(node => { * actual = node; * return false; * }) * ``` */ abstract traverse(consumer: (node: this) => void | boolean): void; /** * Track the path from the this node to the root. * @returns an iterable object, where the first value is parent of the current node * and the last value is the root node. */ abstract track(): Iterable; } export class BasicNode implements Node { readonly #id: string; #parent: this | null = null; readonly #children: Set = new Set(); constructor(id: string) { this.#id = id; } identity(): string { return this.#id; } attach(parent: this): void { if (this.#parent !== null) throw new Error('Node already has a parent.'); parent.appendChild(this as unknown as this); this.#parent = parent; } detach(): void { if (this.#parent === null) throw new Error('Node has no parent.'); this.#parent.removeChild(this as unknown as this); this.#parent = null; } appendChild(child: this): void { this.#children.add(child); } removeChild(child: this): void { this.#children.delete(child); } parent(): this | null { return this.#parent; } children(): ReadonlySet { return this.#children; } dispose(): void { if (this.#parent !== null) this.detach(); for (const child of this.#children) child.dispose(); } traverse(consumer: (node: this) => void | boolean): void { const stop = consumer(this as unknown as this) === false; if (stop) return; for (const child of this.#children) child.traverse(consumer); } *track(): Iterable { let current: this | null = this.#parent; while (current !== null) { yield current; current = current.#parent; } } }