State & Data Flow
How OpenGPEX stores, updates, and tracks all editor state — from layer properties to undo history — with native-refresh-rate performance and sub-2ms undo operations.
Design Overview
User Gesture (mouse/touch)
│
▼
Interaction Dispatcher
│
├─── rAF path ─────► Fast Track (volatile ref, no React re-render)
│ │
│ ▼
│ Canvas repaints immediately (rAF)
│
└─── Commit path ──► Reducer (Immer produce)
│
├──► Undo Patch recorded (JSON Patch, ~100 bytes)
│
▼
React re-render (selective via selectors)
Normalized State
All domain entities use a { byId, order } dictionary pattern:
byId— Hash map keyed by entity ID, providing O(1) lookupsorder— String array defining rendering Z-order
This design enables:
- O(1) updates — Direct property mutation without tree traversal
- Efficient reordering — Only the order array changes; objects stay referentially stable
- Minimal undo patches — Inserting a layer produces exactly 2 patches instead of shifting all indices
Dual-Track Architecture (Fast Track + Slow Track)
| Slow Track (Persistent) | Fast Track (Volatile) | |
|---|---|---|
| Backing | React useReducer + Immer |
Mutable useRef |
| Drives | UI panels, layer tree | Canvas paint, overlays |
| Triggers | React re-render | requestAnimationFrame only |
| Undo | Produces patches on every dispatch | Produces nothing until commit |
| Persist | IndexedDB | Never |
During a drag interaction:
- Fast Track receives pointer-move updates every animation frame (60–120fps, no React involved)
- Canvas reads merged state (persistent + volatile) and repaints immediately
- On pointer-up, volatile state commits to Slow Track as one atomic write
- One undo patch is generated for the entire drag operation (not one per frame)
4-Layer Dispatch
All state mutations flow through one of four channels:
| Layer | Name | Use Case | Characteristics |
|---|---|---|---|
| 1 | Atomic | Simple property changes (opacity, name) | Direct reducer dispatch, auto undo |
| 2 | Fast Track | High-frequency interactions (drag, resize, pan) | Volatile ref, commit-on-end |
| 3 | Facade | Multi-step operations (rotate, merge, crop) | Groups into single undo step |
| 4 | Command Bus | Cross-plugin messaging, keyboard shortcuts | UID-addressed, async, extensible |
Undo/Redo
Built on Immer JSON Patches — recording only the minimal property-level diff:
| Metric | Traditional (Full Snapshot) | OpenGPEX (Patches) |
|---|---|---|
| Memory per step | 1–10 MB | 50–200 bytes |
| Undo latency | 10–50ms (deep copy) | <2ms (patch apply) |
| 100 undo steps | 100–1000 MB RAM | ~20 KB RAM |
Additional design choices:
- Viewport stability — Camera state is excluded from undo to prevent jarring jumps
- History limit — 50 steps max, oldest discarded automatically
- Fast Track integration — Dragging 500px produces exactly 1 undo step, not 500
Last updated: 2026-07-30