This document describes the internal architecture, data pipeline, identity model, and structural guarantees of JSON Flow. It is intended for contributors and developers working on the codebase.
For user-facing documentation, see README.md.
- System Overview
- Identity Model
- Tree Construction
- Layout System
- Search Engine
- Collapse Model
- Projection Model
- Performance Characteristics
- Structural Guarantees
JSON Flow operates as a three-layer system: Extension Host, Webview, and Web Worker. Data flows through a linear pipeline from parse to render.
Extension Host: parse file → send parsed JSON to Webview
↓
Webview: dispatch to Worker (debounced)
↓
Worker: generateTree → getRootId → layoutElementsCore → return positioned nodes + edges
↓
Webview: workerNodes / workerEdges (immutable from this point)
↓
Collapse filter: visibleNodes = workerNodes.filter(n => !collapsedNodes.has(n.id))
↓
Parent map: buildParentMap(treeData) → child-to-parent Map<string, string>
↓
Search: computeMatches(term, allNodes, labelIndex) → matchIds
↓
Projection: searchContextSet derived per mode (highlight / focus-context / focus-strict)
↓
useSearchProjection: filter visibleNodes by searchContextSet, enrich with isSearchMatch
↓
renderNodes / renderEdges → ReactFlow
Extension Host - Runs inside the VS Code process. Parses all supported file formats, handles commands and file system operations, manages Live Sync state and format gating, validates all messages received from the webview, and handles localization. The extension host never renders UI and never runs layout algorithms.
Webview - A sandboxed iframe running React. Renders the interactive graph, manages UI state (collapse, search, projection, settings), persists appearance settings, and computes collapse filtering, search matching, and projection. Communicates with the extension host exclusively via structured message passing. Has no access to the VS Code API, the filesystem, or extension host state.
Web Worker - The sole layout authority. Receives parsed JSON data and a layout direction. Executes the layout pipeline deterministically via generateTree(), getRootId(), and layoutElementsCore(). Returns positioned nodes and edges. Stateless: processes each request independently with no state preserved between requests. Contains no DOM dependencies or React runtime. No layout logic runs in the main thread.
Every node in the graph has a deterministic identity based on its structural position in the source data. Two identity domains exist and remain disjoint.
All data nodes are identified by RFC 6901 JSON Pointer strings (e.g. /users/0/name). Pointers are constructed exclusively via buildPointer() from src/shared/node-pointer.ts. This function is shared across the Worker, the main thread, and the extension host. Manual pointer concatenation does not occur anywhere in the codebase.
The graph structural root uses a sentinel identifier GRAPH_ROOT_ID (value: '__GRAPH_ROOT__'), defined in src/shared/graph-identity.ts. This sentinel is intentionally not a valid JSON Pointer - it does not start with / - so it can never collide with any RFC 6901 pointer derived from document content.
A development-only assertion in generateTree() verifies at runtime that GRAPH_ROOT_ID does not start with /.
Node IDs are stable across re-renders for the same file content. Identity survives Worker restart given unchanged document content. This enables reliable preservation of collapse state, search results, and selection during updates.
Tree construction is performed by generateTree() in webview/helpers/generateTree.ts. This function runs inside the Web Worker.
generateTree() takes a parsed JSON value and recursively produces a TreeMap (a Record<string, TreeNode>). Each node receives:
- An
idbased on its JSON Pointer path viabuildPointer(). - A
nameused as the display label. - A
dataobject containingtype(JavaScripttypeofvalue) andline(estimated line number). - An optional
childrenarray of child node IDs.
The graph root node receives GRAPH_ROOT_ID as its ID and GRAPH_ROOT_LABEL ('Root') as its name. When the current node is the graph root, child JSON Pointers are built relative to POINTER_ROOT ('/'), not relative to the sentinel.
- Branch nodes (objects and arrays): Named by their key segment via
lastSegment(parentId), orGRAPH_ROOT_LABELfor the root. - Leaf nodes: Named using the format
key: value(with": "as separator). This exact format is a contract:searchService.extractKey()andsearchService.extractValue()depend on it.
Each node stores an estimated line number in data.line. This value is approximated during tree construction and used for display purposes.
- Pure function: identical input produces identical output.
- Single recursive pass: O(N) where N is the total number of JSON values.
- Accumulator-based: builds the TreeMap in a single shared
accobject to avoid intermediate allocations.
The Web Worker is the sole layout authority. Layout computation occurs exclusively inside the Worker via layoutElementsCore() in webview/services/layout-core.ts. No layout logic runs in the main thread or the extension host.
layoutElementsCore() selects a layout algorithm based on node count:
- ≤ 2000 nodes (
LARGE_GRAPH_THRESHOLD):layoutFromMap()usingentitree-flexfor hierarchical layout with optimal edge routing and spacing. - > 2000 nodes:
layoutLinearSimple()using O(N) breadth-first positioning for responsive performance on large documents.
Both paths produce identical output shape via shared createNode() and createEdge() functions. entitree-flex is the sole external layout dependency.
createNode() produces a ReactFlow node with:
data.label: display label from the tree node'sname.data.type: the JavaScripttypeofvalue fromtree[node.id]?.data?.type.data.direction: layout direction.data.isRoot: whether the node is the graph root.data.line: source line number.
Worker output arrays (workerNodes, workerEdges) are treated as immutable after return. All downstream stages (collapse filtering, search projection, edge settings) produce new arrays and new node objects. The Worker-defined node ordering is preserved through the entire pipeline.
Search is implemented in webview/services/searchService.ts as a set of pure functions with no React dependencies or side effects.
parseSearchTokens(term) splits the search string on whitespace and classifies each segment into a ParsedToken:
| Kind | Prefix | Behavior |
|---|---|---|
text |
(none) | Case-insensitive substring match against node label |
key |
key: |
Match the key portion of a leaf label (before ": ") |
value |
value: |
Match the value portion of a leaf label (after ": ") |
type |
type: |
Exact match against node.data.type |
path |
path: |
Case-insensitive substring match against node ID |
depth |
depth>, depth<, depth= |
Compare depth (from JSON Pointer segmentation) against threshold |
Tokens are combined with AND semantics: a node matches only when all tokens evaluate to true.
extractKey(label) returns the portion before the first ": " separator, or the full label if no separator exists. extractValue(label) returns the portion after the first ": " separator, or empty string. These functions depend on the ": " format contract established by generateTree().
getDepth(nodeId) counts / segments in the node ID. IDs not starting with / (i.e. GRAPH_ROOT_ID) return depth 0.
computeMatches(term, nodes, labelIndex) parses the term into tokens and evaluates each node. For single plain-text tokens, an exact full-label index (labelIndex) provides a fast-path lookup. Otherwise, all nodes are filtered by evaluating all tokens per node.
Hidden Match Handling
Match computation runs against the full Worker node set (allNodes). The result is partitioned in the UI layer: nodes present in the post-collapse visible set are visible matches; nodes absent are hidden matches. The hidden count is displayed as (+N hidden) in the search counter. When all matches are hidden, an indicator banner is shown.
Collapse is managed by useFlowController in the webview. State is a Set<string> of collapsed node IDs managed via useReducer with actions: COLLAPSE_NODES, EXPAND_NODES, TOGGLE_NODE, RESET.
buildParentMap(tree) in webview/services/treeService.ts builds a Map<string, string> (child → parent) in a single O(N) pass over the tree. This map is used for:
computeNodesWithCollapsedDescendants(): walks up from each collapsed node to find ancestor nodes that should display a collapse indicator. Short-circuits on already-visited ancestors.collectAncestors(): walks up from search matches to collect ancestor IDs forfocus-contextprojection. Short-circuits on already-visited ancestors, ensuring each node is visited at most once across all walks.
getDescendantsOf(nodeId, tree) in webview/services/treeService.ts collects all descendant IDs via iterative stack-based traversal. Complexity is O(subtree). This function is called on-demand per collapse toggle - there is no persistent descendant cache or eager precomputation.
computeNodesWithCollapsedDescendants(collapsedNodes, parentMap) returns the set of node IDs that have at least one descendant in the collapsed set. This is used to render the collapse indicator on ancestor nodes.
Collapse filtering precedes search and projection. The pipeline order is:
- Worker produces
workerNodes. - Collapse filter produces
visibleNodes = workerNodes.filter(n => !collapsedNodes.has(n.id)). - Search operates on
allNodes(full Worker output) and producesmatchIds. - Projection operates on
visibleNodesandmatchIds.
Projection is computed by useSearchProjection in webview/hooks/useSearchProjection.ts. It derives renderNodes and renderEdges from visibleNodes and workerEdges.
A Set<string> | null computed upstream in FlowCanvas:
- No active search:
null(no filtering applied). highlightmode:null(all visible nodes remain; match state is applied per-node).focus-contextmode:matchIds ∪ collectAncestors(matchIds, parentMap).focus-strictmode:matchIdsonly.
When searchContextSet is non-null, visibleNodes are filtered to only those present in the set.
computeSearchMatch(nodeId, searchMatchIds, searchProjectionMode) returns a tri-state value per node:
| Mode | Match | Non-match | No search |
|---|---|---|---|
highlight |
true (ring) |
false (opacity 50%) |
undefined |
focus-context |
true (ring) |
undefined (ancestor, normal) |
undefined |
focus-strict |
undefined (all projected are matches) |
- | undefined |
Edges are filtered to those where both source and target are present in the projected node set. Edge settings (type, animation, arrow) are applied after filtering.
useSearchProjection produces new node objects via spread ({ ...node, data: { ...node.data, ... } }) and new edge arrays. Worker output is never mutated.
All stages of the pipeline are designed to operate in linear time relative to the number of nodes.
- Tree construction (
generateTree): Single recursive pass over the parsed JSON. O(N). - Layout (
layoutElementsCore): O(N) for both layout strategies. The hierarchical layout delegates toentitree-flex; the linear layout uses breadth-first positioning. - Collapse filter: Single
.filter()pass overworkerNodes. O(N). - Parent map (
buildParentMap): Single pass over tree nodes and their children arrays. O(N). - Search (
computeMatches): Evaluates each token per node. O(N × T) where T is the number of tokens. Each token evaluation is O(1). - Ancestor collection (
collectAncestors): Walks parent chain per match. O(N) amortized - each node is visited at most once across all walks. - Descendant collection (
getDescendantsOf): O(subtree) per toggle invocation. On-demand, not cached. - Projection (
useSearchProjection): Filters and mapsvisibleNodes. O(N).
There is no quadratic computation in the pipeline. No persistent descendant storage is maintained. Memory usage is proportional to tree size: the parent map holds one entry per non-root node, and the collapsed-nodes set holds only explicitly collapsed node IDs.
Performance depends on structural complexity (depth, branching factor), not only file size. There is no hard file size cap.
The following properties are assumed and maintained throughout the system. They are foundational to the correct operation of identity, collapse, search, projection, and rendering.
- The system assumes
GRAPH_ROOT_IDdoes not start with/. It belongs to a separate domain from JSON Pointer IDs and cannot collide with any pointer derived from document content. - The system assumes all data node IDs are valid RFC 6901 JSON Pointers, constructed exclusively via
buildPointer(). - The system assumes manual pointer concatenation does not occur.
- The system assumes
generateTree()is pure: identical input produces identical output.
- The system assumes each node carries
data.label(display label) anddata.type(JavaScripttypeofvalue), both set explicitly duringcreateNode()from the source tree. - The system assumes leaf node labels use the
": "separator format.searchService.extractKey()andsearchService.extractValue()depend on this.
- The system assumes collapse filtering precedes projection. The pipeline order is: Worker → collapse → search → projection → render.
- The system assumes Worker output arrays are never mutated. All downstream stages produce new arrays and new node objects.
- The system assumes Worker-defined node ordering is preserved through the pipeline.
- The system assumes projection does not trigger layout recalculation.
- The system assumes search matching operates on the full Worker output; visibility partitioning uses the post-collapse node set.
- The system assumes the Worker is the sole layout authority. No layout logic runs outside the Worker.
- The system assumes the Worker remains stateless between requests. Workers may be created or destroyed at any time.
- The system assumes
LARGE_GRAPH_THRESHOLDis fixed at 2000. Both layout paths produce identical output shape via sharedcreateNode()/createEdge().
- The system assumes collapse state is local to the webview, managed as a
Set<string>viauseReducer. - The system assumes collapse derivation walks upward via
parentMap, not via a descendants cache. - The system assumes the
RESETaction exists but is not dispatched on dataset swap. Stale collapse IDs referencing non-existent nodes are handled gracefully by the filter.