Umbrella refactor that structurally fixes a family of filed bugs (save-error swallowing, rename race, StrictMode double-undo, import validation, write races) rather than patching each one. Concrete shape:
1. Reducer-based project store
useProject.ts (1,115 lines) couples three useStates (project, undoStack, redoStack) with timers and cross-updater side effects — the root cause of the StrictMode double-push bug. Replace with one reducer over { past, present, future } exposed via context. App.tsx currently drills 30+ props into ResultPanel and ~20 into SheetPanel; a store with selector hooks removes most of that. (App.tsx at 1,543 lines also inlines SheetTab, the multi-page print generator at :762-920 → utils/print.ts, tutorial persistence → useTutorial, and file I/O.)
2. A persistence module that owns writing
- Serialized write queue: today each debounced
persist fires an independent async OPFS write; createWritable commits on close(), so a slow older write can land after a newer one and win — stale data persisted even single-tab (useProject.ts:196-199). Chain writes through a single promise queue.
- Errors propagate up (see the data-loss issue),
pagehide flush, navigator.storage.persist() requested once.
- Multi-tab: two tabs auto-open the same last-project and clobber each other every 500 ms, silently. Web Locks (
navigator.locks) or a BroadcastChannel "this tab owns project X" guard is cheap insurance.
3. Versioned schema + validation at the boundary
version: number on Project, a migration ladder, structural validation on import/load (see the undo/import issue for the concrete failure modes).
4. Images as OPFS blobs, not base64 strings in the JSON
This is the highest-leverage change. Today pattern and sheet images are data URLs embedded in project state, which means:
- every debounced autosave
JSON.stringifys multi-MB strings on the main thread (every 500 ms during drags)
- every undo snapshot retains them
- the SAM embedding cache uses the full data-URL as an IndexedDB key (
samWorker.ts:269)
- export via
data: URI triples memory and hits URL limits
Store image binaries as separate OPFS files referenced by ID; project JSON shrinks by orders of magnitude; export becomes a small JSON + image files (or a zip).
Suggested order
(2) write queue + error propagation first (it's the active data-loss risk), then (3) versioning (cheap, unblocks format changes), then (4) image extraction, then (1) the reducer store as the carrier for the rest.
Umbrella refactor that structurally fixes a family of filed bugs (save-error swallowing, rename race, StrictMode double-undo, import validation, write races) rather than patching each one. Concrete shape:
1. Reducer-based project store
useProject.ts(1,115 lines) couples threeuseStates (project, undoStack, redoStack) with timers and cross-updater side effects — the root cause of the StrictMode double-push bug. Replace with one reducer over{ past, present, future }exposed via context. App.tsx currently drills 30+ props into ResultPanel and ~20 into SheetPanel; a store with selector hooks removes most of that. (App.tsx at 1,543 lines also inlinesSheetTab, the multi-page print generator at:762-920→utils/print.ts, tutorial persistence →useTutorial, and file I/O.)2. A persistence module that owns writing
persistfires an independent async OPFS write;createWritablecommits onclose(), so a slow older write can land after a newer one and win — stale data persisted even single-tab (useProject.ts:196-199). Chain writes through a single promise queue.pagehideflush,navigator.storage.persist()requested once.navigator.locks) or a BroadcastChannel "this tab owns project X" guard is cheap insurance.3. Versioned schema + validation at the boundary
version: numberonProject, a migration ladder, structural validation on import/load (see the undo/import issue for the concrete failure modes).4. Images as OPFS blobs, not base64 strings in the JSON
This is the highest-leverage change. Today pattern and sheet images are data URLs embedded in project state, which means:
JSON.stringifys multi-MB strings on the main thread (every 500 ms during drags)samWorker.ts:269)data:URI triples memory and hits URL limitsStore image binaries as separate OPFS files referenced by ID; project JSON shrinks by orders of magnitude; export becomes a small JSON + image files (or a zip).
Suggested order
(2) write queue + error propagation first (it's the active data-loss risk), then (3) versioning (cheap, unblocks format changes), then (4) image extraction, then (1) the reducer store as the carrier for the rest.