Skip to content

Persistence

The scene-builder auto-saves your work to the browser's IndexedDB and localStorage. When you reopen the page, everything is restored exactly as you left it.

What Gets Saved

IndexedDB (debounced 500ms)

The sajou-scene-builder database has 9 object stores:

StoreContent
sceneScene dimensions, background, layers, positions, routes, zones
entitiesEntity definitions (sprites, spritesheets, GIFs)
choreographiesChoreography definitions, steps, timing
wiresPatch bay wire connections
bindingsEntity bindings to choreography commands
timelineSignal timeline state
shadersShader definitions, sources, uniforms
assetsImage files as ArrayBuffer (incremental save)
p5Sketch definitions (p5.js + Three.js), source code, params, mode

DB_VERSION history: 1 (initial) → 2 (added shaders store) → 3 (added p5 store).

Each store wraps its data in a versioned envelope { version: 1, data: ... }.

localStorage (debounced 300ms)

KeyContent
sajou:remote-sourcesRemote signal source configs (URL, protocol, API key, model)
sajou:editor-prefsPanel layouts, grid settings, view mode, pipeline layout
sajou:server-urlServer URL override (empty = Vite proxy default). Set via the connection popover.

Not Persisted

These are rebuilt or re-discovered on each session:

  • Undo/redo stack
  • Local signal sources (re-discovered via local discovery)
  • Signal source connection status (all sources start "disconnected")
  • Server connection status (re-probed on startup, tracked by server-connection.ts)
  • Active selections
  • Compositor state

Save Flow

State change (any store)

    ├── IndexedDB stores ──→ debounce 500ms ──→ dbPut("store", "current", { version: 1, data })

    └── localStorage stores ──→ debounce 300ms ──→ localStorage.setItem(key, JSON.stringify(...))

Multiple rapid changes within the debounce window collapse into a single write.

On page unload (beforeunload), all pending debounced saves are flushed immediately.

Restore Flow

restoreState() runs before the workspace is initialized:

  1. Check if scene store has data -- if not, this is a first launch
  2. Restore scene state (dimensions, background, layers, etc.)
  3. Restore entity definitions
  4. Restore choreography state (clear selection)
  5. Restore wiring state (clear drag state)
  6. Restore binding state
  7. Restore signal timeline (clear selection)
  8. Restore shader state (clear selection, reset playing: true)
  9. Restore assets: ArrayBufferFileURL.createObjectURL()objectUrl
  10. Restore remote sources from localStorage
  11. Restore editor preferences from localStorage
  12. Clear undo stack (restored state has no history)

Asset Persistence

Assets are saved incrementally -- only new paths not already in IndexedDB are written. Each asset is stored as:

typescript
{
  name: string;         // original filename
  path: string;         // unique asset path (key)
  category: string;     // "sprites" | "spritesheets" | "gifs"
  format: AssetFormat;  // "png" | "svg" | "webp" | "gif" | "jpeg"
  buffer: ArrayBuffer;  // raw file data
  naturalWidth?: number;
  naturalHeight?: number;
  frameCount?: number;
  detectedFps?: number;
}

On restore, ArrayBuffer is reconstructed into a File object and a fresh objectUrl is created.

Force Persist

forcePersistAll() bypasses debouncing and writes all stores immediately. It's called after import to ensure the imported scene is fully saved.

Selective Import & Export

Scene files use the .sajou extension (ZIP archive internally). Both import and export show a section picker dialog — you choose which sections to include. Older .zip files are also accepted on import.

SectionStores affected
Visual layoutScene state (placements, positions, routes, lighting, particles)
Entities & AssetsEntity definitions, asset files
Choreographies & WiringChoreography definitions, wire connections, bindings
ShadersShader definitions
SketchesSketch definitions (p5.js + Three.js), source code, params

Sections with no content are grayed out. Unchecked sections keep their current state on import (or are omitted from the export).

On import, the dialog shows summary counts for each section and contextual warnings (e.g. "Visual layout without Entities may produce invisible meshes"). After import, autoWireConnectedSources() creates signal -> signal-type wires for any connected sources, so imported choreographies work immediately.

New Scene

The "New" button (Ctrl+N) triggers newScene():

  1. Clear all IndexedDB stores (dbClearAll())
  2. Remove localStorage keys
  3. Reset all in-memory stores to defaults
  4. Clear undo stack
  5. Re-discover local sources (scanAndSyncLocal())

Key Files

FileRole
state/persistence-db.tsIndexedDB wrapper (singleton connection, CRUD helpers)
state/persistence.tsAuto-save/restore orchestrator
io/export-scene.tsExport to .sajou archive (selective)
io/export-dialog.tsExport section picker dialog
io/import-scene.tsImport from .sajou / .zip (4-phase: pick, parse, dialog, apply)
io/import-dialog.tsImport section picker dialog
state/auto-wire.tsAuto-wire connected sources on import/connect