Skip to content

Refactor: project persistence & state architecture — reducer store, versioned schema, serialized write queue, images out of the JSON, multi-tab safety #101

Description

@dompm

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-920utils/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions