Files
lemonspace_app/docs/plans/2026-04-03-canvas-modularization-design.md

217 lines
8.2 KiB
Markdown

# Canvas Modularization Design
## Goal
Modularize `components/canvas/canvas.tsx` so the file becomes easier to read, easier to test, and easier for a second engineer to navigate without changing user-visible canvas behavior or existing public component contracts.
## Why Now
- `components/canvas/canvas.tsx` currently combines at least four different responsibilities: sync orchestration, Convex-to-React-Flow reconciliation, interaction handlers, and render composition.
- The file is still the main knowledge bottleneck in the canvas engine even after earlier helper extractions.
- A second developer joining during the seed phase will need clearer ownership boundaries and faster onboarding into the canvas hot path.
## Current Findings
- Sync and optimistic update orchestration are concentrated around the mutation and queue layer in `components/canvas/canvas.tsx:258` and `components/canvas/canvas.tsx:984`.
- Convex-to-local reconciliation for nodes and edges lives in large `useLayoutEffect` blocks in `components/canvas/canvas.tsx:1922`.
- Interaction logic for drag, connect, reconnect, drop, scissor mode, and highlighting is spread across the lower half of the file in `components/canvas/canvas.tsx:1798`, `components/canvas/canvas.tsx:2182`, `components/canvas/canvas.tsx:2555`, and `components/canvas/canvas.tsx:2730`.
- Render composition is comparatively simple, but it is buried under orchestration code in `components/canvas/canvas.tsx:2901`.
## Context7 Guidance Applied
- React guidance: extract non-visual logic into custom hooks so components focus on rendering and explicit data flow.
- React guidance: keep hook composition clear and stable rather than hiding too much behavior inside a generic controller.
- React Flow guidance: keep `nodeTypes` stable outside render, preserve `ReactFlowProvider` boundaries, and keep controlled flow state explicit when splitting handlers.
## Chosen Approach
Use a thin-orchestrator refactor:
1. Keep `components/canvas/canvas.tsx` as the top-level canvas entry and wiring layer.
2. Extract the largest logic clusters into domain-specific hooks with explicit inputs and outputs.
3. Leave existing external APIs unchanged: `Canvas`, `CanvasInner`, provider usage, mutation contracts, and React Flow integration stay behaviorally identical.
4. Prefer hook boundaries over a single giant controller or reducer in phase 1.
This approach gives the best trade-off between readability, testability, and migration risk.
## Rejected Alternatives
### 1. Single `useCanvasController` hook with reducer
- Pros: very strong test surface and centralized state transitions.
- Cons: too risky for the current canvas because drag locks, optimistic ID handoff, offline replay, and Convex reconciliation are behavior-sensitive and already intertwined with refs and effects.
### 2. Minimal handler extraction only
- Pros: lowest refactor risk.
- Cons: improves file length but not ownership or conceptual clarity; `canvas.tsx` would still remain the main cognitive bottleneck.
## Target Architecture
### 1. `canvas.tsx` becomes the orchestrator
`components/canvas/canvas.tsx` should keep only:
- top-level provider wiring
- hook composition
- final prop assembly for the rendered canvas view
- small glue callbacks when multiple hooks need to meet
The file should stop owning detailed effect bodies and long imperative flows.
### 2. Extract domain hooks by responsibility
#### `components/canvas/use-canvas-sync-engine.ts`
Owns:
- online/offline state
- queue flushing and retry scheduling
- deferred mutation handling for optimistic nodes
- optimistic-to-real ID remapping support
- wrappers like `runMoveNodeMutation`, `runResizeNodeMutation`, `runUpdateNodeDataMutation`, `runCreateEdgeMutation`, and node creation variants
Why:
- This is the most operationally complex part of the file.
- It has clear inputs and outputs and can be tested with mocked mutations and queue helpers.
#### `components/canvas/use-canvas-flow-reconciliation.ts`
Owns:
- Convex edges -> RF edges reconciliation
- Convex nodes -> RF nodes reconciliation
- carry-forward logic for optimistic edges during handoff gaps
- local pinning and merge behavior
- compare-node data resolution and glow-aware edge mapping
Why:
- This logic is mostly deterministic and should become easier to test if helper functions are extracted out of effect bodies.
#### `components/canvas/use-canvas-node-interactions.ts`
Owns:
- `onNodesChange`
- node drag start / drag / drag stop
- resize lock transitions
- intersected-edge highlighting state
Why:
- Today the drag lifecycle is hard to reason about because local visual behavior and persistence behavior are mixed together.
#### `components/canvas/use-canvas-connections.ts`
Owns:
- `onConnect`
- `onConnectEnd`
- connection-drop-menu state
- create-node-from-connection flows
- adapter glue for reconnect handling
Why:
- New connection creation is a separate mental model from drag/drop creation and should be independently understandable.
#### `components/canvas/use-canvas-drop.ts`
Owns:
- `onDragOver`
- `onDrop`
- DnD payload parsing
- file upload drop flow
- create-node-on-drop orchestration
Why:
- Drop behavior has its own error handling, upload behavior, and defaults. It is a natural isolation boundary.
### 3. Optional view extraction in the final phase
Add `components/canvas/canvas-view.tsx` only after the main hook boundaries are stable.
It should stay presentational and receive:
- `nodes`, `edges`
- overlay state
- `ReactFlow` handlers
- toolbar state and callbacks
Why not first:
- Extracting view first reduces file size but leaves the hard behavioral logic untouched.
## Ownership Model For A Two-Engineer Team
The proposed split creates clean ownership zones:
- Engineer A: sync, optimistic state, offline behavior
- Engineer B: interactions, connection creation, drop flows
- Shared: top-level composition in `canvas.tsx` and pure helpers in `canvas-helpers.ts`
This is useful even if the second engineer joins later because it documents the system in code boundaries, not only in docs.
## Testing Strategy
The refactor should raise confidence by moving logic into units that are easier to test.
### Priority test targets
- optimistic node create -> real ID remap
- deferred `resizeNode` and `updateData` for optimistic nodes
- edge carry during the create/handoff gap
- drag lock prevents stale Convex snapshots from overriding local drag state
- connection validation still rejects invalid edges
- drag-and-drop creation still supports both node payloads and file uploads
### Test shape
- Extract deterministic logic from hook internals into pure helpers where possible.
- Add targeted tests for those helpers first.
- Add hook-level tests only for behavior that genuinely depends on refs, effects, or queue timing.
- Keep manual verification for React Flow pointer interactions that are awkward to simulate precisely.
## Refactor Sequencing
Use small, behavior-preserving phases.
### Phase 1: Sync engine extraction
- Highest complexity
- Highest test ROI
- Unlocks simpler downstream hooks because many callbacks become imported capabilities instead of locally defined machinery
### Phase 2: Flow reconciliation extraction
- Separate mapping and merge behavior from interaction code
- Makes the render path easier to inspect
### Phase 3: Connections and drop extraction
- Split creation pathways by intent: connect vs drop
- Reduces the size of the lower half of `canvas.tsx`
### Phase 4: Optional presentational view extraction
- Only if the remaining orchestrator still feels too dense
## Guardrails
- Do not change `Canvas` or `CanvasInner` public props.
- Keep `ReactFlowProvider` and `useReactFlow` usage exactly compatible with current behavior.
- Keep `nodeTypes` outside React components.
- Preserve ordering of side effects and queue mutations.
- Do not introduce new dependencies.
- Do not rewrite into a reducer architecture in this phase.
## Expected Outcome
- `components/canvas/canvas.tsx` becomes an understandable composition root instead of a monolith.
- The highest-risk behaviors remain intact because the refactor follows existing seams rather than inventing a new runtime model.
- A second engineer can work in the canvas codebase by area of concern instead of first learning the entire file.