Delta and Checkpoint History

Overview

History entries are stored as a mix of:

  • Checkpoint entries: full JSON-safe snapshots of objects and canvas size.
  • Delta entries: forward/reverse patches between adjacent states.

This keeps undo/redo deterministic while reducing repeated full snapshots.

Behavior

  • The first saved entry is always a checkpoint.
  • Subsequent saves record deltas when possible.
  • A checkpoint is forced when:
    • the configured delta interval is reached, or
    • cumulative delta bytes exceed the configured threshold.
  • Undo applies reverse patches.
  • Redo applies forward patches.
  • jumpToState(index) reconstructs state from the nearest prior checkpoint.
  • During restore, the editor applies incremental node sync (add/remove/change) instead of always rebuilding every canvas node.
  • Undo/redo restores object data, canvas size, connections, and animation data from the same timeline.
  • Editor selection is preserved after undo/redo when selected object IDs still exist in the restored state.

Export and Import

  • exportHistory() now outputs version 2.0 payloads with entries.
  • importHistory() supports:
    • version 2.0 entries,
    • legacy snapshot payloads (version 1.0) and migrates them to the new model.
  • Imported patches are validated. Invalid path semantics (for example empty replace paths) or dangerous keys are rejected.
  • Entry order is validated during export/import. If a tampered sequence cannot replay forward/reverse consistently, import fails safely.

Persistence Rows (Database-Friendly)

History entries can be flattened into row records with:

  • id
  • docId
  • entryIndex
  • data (JSON.stringify(entry))
  • schemaVersion (currently 2.0)
  • createdAt

Recommended index/constraint strategy:

  • Primary key: id
  • Unique: (docId, entryIndex)
  • Query index: (docId, entryIndex DESC) for pagination/recent reads

Runtime helpers:

  • toPersistedHistoryRows and fromPersistedHistoryRows
  • selectReplayRows (latest checkpoint + trailing deltas)
  • truncateRowsKeepingRecoverableBaseline (keep recent rows without losing replay baseline)
  • pruneObsoletePersistedRows (drop unsupported schema rows and orphan doc rows)
  • Typed errors with codes:
    • ERR_INVALID_HISTORY_PAYLOAD
    • ERR_UNSUPPORTED_HISTORY_SCHEMA

JSON-Safe Constraints

Persisted entries are JSON-safe:

  • Dates are stored as ISO strings.
  • undefined, functions, class instances, and cyclic references are not allowed.
  • Dangerous keys such as __proto__, constructor, and prototype are blocked.

Notes

  • Runtime states still expose Date objects for timestamps and object created/modified fields.
  • Compression can drop no-op delta entries and merge adjacent small deltas within a short time window.

Troubleshooting

Common Error Codes

  • ERR_INVALID_HISTORY_PAYLOAD
    • Typical causes: invalid JSON row data, index gaps, missing baseline checkpoint, unsafe patch path.
    • Recovery: reload latest valid checkpoint window (selectReplayRows) and drop invalid rows.
  • ERR_UNSUPPORTED_HISTORY_SCHEMA
    • Typical causes: old/unknown schema version rows.
    • Recovery: run migration path to 2.0, or skip unsupported rows using pruneObsoletePersistedRows.

Minimal Recovery Playbook for Corrupted History

  1. Load rows for one docId and sort by entryIndex.
  2. Run pruneObsoletePersistedRows (filter schema + orphan docs).
  3. Run selectReplayRows for target index to keep checkpoint + deltas.
  4. Parse with fromPersistedHistoryRows; if this fails, fallback to the last known valid checkpoint row only.
  5. Rebuild active history payload and continue editing from that baseline.

Migration and Cross-Platform Notes

  • importHistory supports legacy snapshot payload (v1) and migrates to v2 entries.
  • Persisted row format is plain JSON text and index-based ordering; row order can be reconstructed on any platform by sorting entryIndex.