Understanding the NodeTree Primitive and Tree‑Agnostic Mutations in Instatic
Instatic’s NodeTree primitive represents hierarchical structures as a flat map of nodes keyed by UUID for O(1) lookups, while tree‑agnostic mutations are pure functions defined in src/core/page-tree/mutations.ts that manipulate this structure generically without recursion, enabling consistent operations across pages, visual components, and slot instances.
Instatic, an open-source visual editor maintained by CoreBunch, unifies every hierarchical structure—pages, visual components (VCs), and slot‑fill fragments—under a single data model called NodeTree. This primitive, defined in src/core/page-tree/treeSchema.ts, eliminates expensive tree traversals by storing nodes in a flat Record rather than nested objects. By combining this architecture with tree‑agnostic mutations, the system provides a uniform API that works identically across the React editor, plugin sandbox, and server‑side runtimes.
The NodeTree Primitive Structure
Flat Map Architecture
At its core, the NodeTree interface in src/core/page-tree/treeSchema.ts uses a flat map pattern that decouples storage hierarchy from logical hierarchy:
export interface NodeTree<TNode extends BaseNode = BaseNode> {
nodes: Record<string, TNode> // O(1) lookup of any node by its id
rootNodeId: string // entry point for traversals
}
nodes: ARecordobject where every node (page nodes, VC nodes, slot instances) is keyed by a UUID string. This enables constant‑time lookups without recursive walks.rootNodeId: Specifies the entry point for tree traversals, allowing multiple logical trees to coexist in the same store.- Generics: The type parameter
TNodelets developers work with enriched node types—such asPageNodewithdynamicBindings—while the runtime storage remains compatible with the base schema.
Schema Validation
The NodeTreeSchema (also in treeSchema.ts) validates the shape at the persistence boundary, guaranteeing that every stored tree conforms to the BaseNode schema defined in src/core/page-tree/baseNode.ts. Because concrete node types extend BaseNode, they automatically satisfy the validation requirements without additional boilerplate.
Tree‑Agnostic Mutation API
All mutation helpers live in src/core/page-tree/mutations.ts and accept a NodeTree<TNode> draft. They manipulate the flat map directly without knowing whether the tree represents a page, a visual component, or a slot fragment. This design makes the same API usable for page editing, VC editing, and slot‑fill handling.
Core Mutation Functions
The mutations cover the full lifecycle of tree manipulation:
createNode(moduleId, defaults?)– Generates a fresh node with a nano‑id, empty children array, andparentId: null.insertNode(tree, node, parentId, index?)– Adds a node under the specified parent, updating theparentIdreference and splicing into the child array at the given index.deleteNode(tree, nodeId)– Removes a node and all descendants viadeleteSubtree, ensuring no orphaned references remain.updateNodeProps(tree, nodeId, patch)– Performs a shallow merge of the patch object intonode.props.setBreakpointOverrideandclearBreakpointOverride– Store per‑breakpoint style overrides on individual nodes.renameNode,toggleNodeLocked,toggleNodeHidden– Modify metadata flags without affecting the tree structure.moveNode(tree, nodeId, newParentId, newIndex)– Re‑parents a node while preventing cyclic references and updating child arrays.duplicateNode(tree, nodeId, options?)– Deep‑clones a subtree, generates fresh UUIDs for all descendants, and inserts the clone adjacent to the original.pasteSubtree(tree, payload, parentId, index?, options?)– Inserts foreign subtrees (e.g., from clipboard) with fresh IDs to avoid collisions.wrapNodeandwrapNodes– Enclose one or many nodes inside a new container module, automatically parenting the wrapped nodes.moveNodes– Accepts a selection of top‑level nodes and moves them to a new parent while preserving their relative order.
The Operation Dispatcher
For contexts requiring dynamic operation routing, applyTreeOperation(tree, op) serves as the single entry point used by both the editor store and the plugin VM. This function accepts a tagged union defined in src/core/page-tree/operationSchema.ts and dispatches to the appropriate mutation function above, returning the affected node IDs.
Performance and Safety Guarantees
All mutation functions are pure‑mutative: they receive a draft object (typically from Zustand’s Mutative middleware), mutate it in‑place, and return nothing. Because they operate on the flat map, they never recurse over the entire tree. For example, linkChildrenParents (a helper used during cloning) re‑links a subtree in O(subtree) time rather than O(tree). The mutations also validate existence (e.g., checking parent lookup) and guard against illegal operations such as root deletion or cyclic moves using helper functions from src/core/page-tree/selectors.ts like getParent, isAncestor, and collectSubtreeIds.
Practical Implementation Examples
Below are complete, runnable snippets demonstrating how to manipulate a page tree using the tree‑agnostic API:
import { createNode, insertNode, moveNode, wrapNode } from '@core/page-tree';
import type { NodeTree, PageNode } from '@core/page-tree';
// 1. Create a new paragraph node with initial text
const paragraph = createNode('core.paragraph', { text: 'Hello world' });
// 2. Insert it as the last child of the page’s root node
insertNode(pageTree as NodeTree<PageNode>, paragraph, pageTree.rootNodeId);
// 3. Move the paragraph under a different container (e.g., a column)
moveNode(pageTree as NodeTree<PageNode>, paragraph.id, columnNodeId, 0);
// 4. Wrap the paragraph in a new "section" container
const wrapperId = wrapNode(pageTree as NodeTree<PageNode>, paragraph.id, 'core.section');
These functions are exported from src/core/page-tree/mutations.ts and can be invoked from the React visual editor via Zustand actions or from plugin code via the applyTreeOperation dispatcher.
Summary
- NodeTree stores hierarchical data as a flat
Record<string, TNode>insrc/core/page-tree/treeSchema.ts, enabling O(1) node lookups and eliminating recursive traversals. - Tree‑agnostic mutations in
src/core/page-tree/mutations.tsprovide a unified API for create, insert, delete, move, duplicate, wrap, and paste operations that work on any node type extendingBaseNode. - Pure‑mutative functions operate on draft objects, touching only the relevant subtree for O(subtree) performance while maintaining parent/child references via helpers in
src/core/page-tree/selectors.ts. - Generic type safety allows the editor, plugin VM, and server to share the same mutation logic while working with specialized node types like
PageNode.
Frequently Asked Questions
What makes NodeTree different from a traditional nested tree structure?
Traditional nested trees require recursive traversal to locate or update nodes, resulting in O(depth) or O(tree) complexity. Instatic’s NodeTree flattens the hierarchy into a Record map where every node is accessible by its UUID in constant time. The rootNodeId and parentId references reconstruct the logical hierarchy, while mutations manipulate only the affected nodes and their immediate children.
How do tree-agnostic mutations handle type safety with different node types?
The mutation functions use the generic NodeTree<TNode> interface where TNode extends BaseNode. When calling insertNode or moveNode on a NodeTree<PageNode>, TypeScript enforces that the node argument conforms to PageNode. The runtime storage remains compatible because all concrete types extend the BaseNode schema validated by NodeTreeSchema in src/core/page-tree/treeSchema.ts.
What prevents cyclic references when moving nodes in Instatic?
The moveNode function in src/core/page-tree/mutations.ts guards against cycles by checking whether the target parent is a descendant of the node being moved. It utilizes isAncestor from src/core/page-tree/selectors.ts to verify the lineage before performing the reparenting operation, ensuring that a node cannot be moved into its own subtree.
Can tree-agnostic mutations be used outside the React editor context?
Yes. Because the mutations are pure functions that operate on a draft NodeTree object, they have no dependency on React or the browser DOM. They can be executed in the plugin VM sandbox, in server‑side rendering contexts, or in Node.js scripts by importing directly from @core/page-tree and using the applyTreeOperation dispatcher or individual mutation helpers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →