Understanding the NodeTree Primitive and Tree-Agnostic Mutations in Instatic
Instatic's NodeTree primitive represents every hierarchical structure as a flat map of nodes, enabling O(1) lookups and tree-agnostic mutations that work uniformly across pages, visual components, and slot-fill fragments.
In the Instatic visual editor, all tree-based data—from page hierarchies to visual component instances—flows through a single architecture defined in src/core/page-tree/treeSchema.ts. This design eliminates the need for recursive tree traversals by flattening the hierarchy into a map-based structure, while the mutation API in src/core/page-tree/mutations.ts provides generic operations that remain agnostic to the specific content type being edited.
The NodeTree Primitive Architecture
The NodeTree<TNode> interface serves as the universal container for hierarchical data throughout the Instatic codebase. Unlike traditional nested tree structures, this primitive uses a flat-map pattern that stores every node in a flat dictionary keyed by UUID.
Flat-Map Storage Design
According to the source code in src/core/page-tree/treeSchema.ts, the NodeTree interface consists of two essential properties:
export interface NodeTree<TNode extends BaseNode = BaseNode> {
nodes: Record<string, TNode>
rootNodeId: string
}
nodes: ARecord<string, TNode>providing O(1) lookup for any node by its unique identifier. This eliminates recursive traversal when accessing specific items.rootNodeId: A string indicating the entry point for tree traversals, defining where the hierarchy begins.
This flat structure persists through NodeTreeSchema, which validates data at the persistence boundary to ensure all stored trees conform to the BaseNode schema requirements.
Generic Type Safety
The NodeTree interface accepts a generic type parameter TNode extends BaseNode, allowing callers to work with specialized node types while maintaining runtime compatibility. For example, PageNode extends BaseNode with additional properties like dynamicBindings, yet stores within the same NodeTree container.
Schema Validation
The NodeTreeSchema (also defined in treeSchema.ts) guarantees that any loaded tree conforms to the expected shape. Because concrete node types extend BaseNode, they automatically satisfy the schema validator without requiring custom validation logic for each node variant.
Tree-Agnostic Mutation API
All mutation helpers reside in src/core/page-tree/mutations.ts and operate on NodeTree<TNode> regardless of whether the tree represents a page, a visual component, or a slot-fill fragment. This architecture ensures that create, insert, move, and delete operations work identically across the React-based editor and the plugin VM sandbox.
Core Mutation Functions
The mutation library exposes pure functions that receive a draft NodeTree<TNode> (typically from Zustand's Mutative middleware) and mutate it in-place. These include:
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 the child array and parent references.deleteNode(tree, nodeId): Removes a node and all descendants viadeleteSubtree.updateNodeProps(tree, nodeId, patch): Shallow-merges property changes intonode.props.moveNode(tree, nodeId, newParentId, newIndex): Re-parents nodes while preventing cyclic dependencies.duplicateNode(tree, nodeId, options?): Deep-clones subtrees with fresh UUIDs and inserts them adjacent to the original.pasteSubtree(tree, payload, parentId, index?, options?): Inserts foreign tree structures (e.g., clipboard data) with regenerated identifiers.wrapNodeandwrapNodes: Enclose existing nodes within new container modules.setBreakpointOverride/clearBreakpointOverride: Manage responsive style overrides per viewport.renameNode,toggleNodeLocked,toggleNodeHidden: Metadata mutations for editor state.
The Dispatcher Pattern
The applyTreeOperation(tree, op) function serves as the single entry point used by both the editor store and plugin VM. It accepts a tagged union defined in src/core/page-tree/operationSchema.ts and dispatches to the appropriate mutation handler. This dispatcher ensures that all tree modifications flow through a consistent validation and execution path.
Performance and Safety Guarantees
Because mutations operate on the flat map rather than recursive tree structures, they touch only the relevant subtree during operations. The linkChildrenParents helper (utilizing selectors from src/core/page-tree/selectors.ts) re-links cloned subtrees in O(subtree) time rather than O(tree). Safety validations include parent existence checks, cycle detection in moveNode, and prevention of root node deletion.
Practical Implementation Examples
Below are typical patterns for manipulating node trees in Instatic, applicable both in the visual editor and plugin code:
import { createNode, insertNode, moveNode, wrapNode } from '@core/page-tree';
import type { NodeTree, PageNode } from '@core/page-tree';
// Create a new paragraph node
const paragraph = createNode('core.paragraph', { text: 'Hello world' });
// Insert as the last child of the root node
insertNode(pageTree as NodeTree<PageNode>, paragraph, pageTree.rootNodeId);
// Move the paragraph under a different container
moveNode(pageTree as NodeTree<PageNode>, paragraph.id, columnNodeId, 0);
// Wrap the paragraph in a new section container
const wrapperId = wrapNode(pageTree as NodeTree<PageNode>, paragraph.id, 'core.section');
These functions import from src/core/page-tree/mutations.ts and can be invoked directly or through the applyTreeOperation dispatcher when crossing the plugin boundary.
Summary
- NodeTree stores hierarchical data as a flat
Record<string, TNode>with O(1) lookup, defined insrc/core/page-tree/treeSchema.ts. - The tree-agnostic mutation API in
src/core/page-tree/mutations.tsprovides 11+ operations (create, insert, move, duplicate, wrap) that work uniformly across pages, visual components, and slots. - Generic type safety via
TNode extends BaseNodeallows specialized node types while maintaining a unified storage format. - Performance optimizations include draft-based mutations, subtree-only updates, and lazy parent-linking via helper functions.
- Safety mechanisms prevent cyclic moves, validate parent existence, and protect root nodes from deletion.
Frequently Asked Questions
How does the flat-map structure improve performance over nested trees?
The flat-map design stores every node in a Record object keyed by UUID, enabling O(1) direct access without recursive traversal. When mutations occur, the system updates only the affected nodes and their immediate children rather than walking the entire tree structure, resulting in constant-time lookups and linear-time subtree operations.
Can tree-agnostic mutations handle different node types like pages and visual components?
Yes. The NodeTree<TNode> generic accepts any type extending BaseNode, meaning the same insertNode, moveNode, and duplicateNode functions work for PageNode instances, visual component nodes, and slot-fill fragments without modification. The runtime storage remains a plain BaseNode map while TypeScript provides compile-time type safety.
What prevents invalid tree operations like creating cycles or deleting the root?
The mutation functions in src/core/page-tree/mutations.ts include validation logic that checks for parent existence before insertion and validates ancestry relationships during move operations to prevent cycles. The deleteNode function explicitly guards against root node deletion, and moveNode uses selectors from src/core/page-tree/selectors.ts to verify that the new parent is not a descendant of the moved node.
How do plugins access the tree mutation API safely?
Plugins interact with the tree through the applyTreeOperation dispatcher, which accepts a serialized TreeOperation defined in src/core/page-tree/operationSchema.ts. This single entry point validates operations before execution, ensuring that plugin code running in the sandboxed VM can mutate the tree using the same safe, tree-agnostic API available to the core editor.
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 →