constellation-analyzer/docs/PERSISTENCE_PLAN.md
Jan-Henrik Bruhn f56f928dcf Initial commit
2025-10-10 11:15:51 +02:00

9.6 KiB

Local Storage Persistence Plan for Constellation Analyzer

1. Data Format Specification

JSON Schema Structure

interface ConstellationDocument {
  // Metadata
  metadata: {
    version: string;           // Schema version (e.g., "1.0.0")
    appName: string;           // "constellation-analyzer"
    createdAt: string;         // ISO timestamp
    updatedAt: string;         // ISO timestamp
    lastSavedBy: string;       // Browser fingerprint or "unknown"
  };

  // Graph state
  graph: {
    nodes: SerializedActor[];     // Simplified Actor[] without React Flow internals
    edges: SerializedRelation[];  // Simplified Relation[] without React Flow internals
    nodeTypes: NodeTypeConfig[];  // Already serializable
    edgeTypes: EdgeTypeConfig[];  // Already serializable
  };

  // Editor settings (optional - may persist separately)
  editorSettings?: EditorSettings;
}

// Simplified node structure for storage
interface SerializedActor {
  id: string;
  type: string;  // React Flow node type (e.g., "custom")
  position: { x: number; y: number };
  data: ActorData;
  selected?: boolean;
  dragging?: boolean;
}

// Simplified edge structure for storage
interface SerializedRelation {
  id: string;
  source: string;
  target: string;
  type?: string;  // React Flow edge type
  data: RelationData;
  sourceHandle?: string | null;
  targetHandle?: string | null;
}

Rationale:

  • Separate metadata for versioning and migration support
  • Exclude React Flow-specific runtime properties (measured, width, height, etc.)
  • Store minimal required data to reconstruct full state
  • Include timestamps for debugging and conflict resolution

2. Storage Strategy

Architecture Choice: Zustand Middleware Pattern

Recommended approach: Create a custom Zustand middleware that intercepts state changes and persists to localStorage.

Storage Keys Strategy

const STORAGE_KEYS = {
  GRAPH_STATE: 'constellation:graph:v1',      // Main graph data
  EDITOR_SETTINGS: 'constellation:editor:v1', // Editor preferences
  AUTOSAVE_FLAG: 'constellation:autosave',    // Flag for crash recovery
  LAST_SAVED: 'constellation:lastSaved',      // Timestamp
};

Why separate keys:

  • Allow partial updates (save graph independently from settings)
  • Different persistence strategies (graph = debounced, settings = immediate)
  • Easier to manage storage quota

Debouncing Strategy

// Debounce configuration
const DEBOUNCE_CONFIG = {
  DELAY: 1000,              // 1 second after last change
  MAX_WAIT: 5000,           // Force save every 5 seconds
  THROTTLE_NODE_DRAG: 500,  // Faster saves during drag operations
};

Implementation approach:

  • Use lodash.debounce or custom implementation
  • Different debounce times for different operations:
    • Node dragging: 500ms (frequent but predictable)
    • Adding/deleting: 1000ms (less frequent)
    • Typing in properties: 1000ms (standard)
  • Max wait ensures data isn't lost even during continuous editing

When to Save

Auto-save triggers:

  1. Any GraphStore state mutation (nodes, edges, nodeTypes, edgeTypes)
  2. EditorStore settings changes (optional, can be immediate)
  3. Before window unload (emergency save)
  4. After successful import (to persist imported state)

Don't save:

  • Temporary UI state (selectedRelationType, hover states)
  • React Flow internals (viewport, connection state)

3. Loading Strategy

Bootstrap Sequence

1. App starts → Check for stored data
2. Validate schema version
3. If valid: Deserialize → Hydrate store
4. If invalid: Attempt migration OR use defaults
5. If corrupted: Show recovery dialog → Load defaults
6. Set up auto-save listeners

Validation Approach

Use runtime validation to ensure data integrity.

Validation checks:

  • Schema version exists and is supported
  • All required fields present
  • Node IDs are unique
  • Edge source/target references exist in nodes
  • Type references (node.data.type) exist in nodeTypes
  • Color values are valid hex codes

Hydration Process

Initialize store with loaded data, adding back React Flow properties with defaults.


4. Error Handling Strategy

Error Categories

enum PersistenceError {
  QUOTA_EXCEEDED = 'quota_exceeded',
  CORRUPTED_DATA = 'corrupted_data',
  VERSION_MISMATCH = 'version_mismatch',
  PARSE_ERROR = 'parse_error',
  STORAGE_UNAVAILABLE = 'storage_unavailable',
}

Error Recovery Strategies

Error Strategy User Experience
Quota Exceeded 1. Show warning
2. Compress data (remove whitespace)
3. Offer export to file
4. Continue without auto-save
Toast notification: "Storage full. Save to file to preserve work."
Corrupted Data 1. Attempt partial recovery
2. Load default state
3. Log error for debugging
4. Offer to restore from backup
Dialog: "Previous session corrupted. Starting fresh." + Show details
Version Mismatch 1. Attempt migration
2. If migration fails, load defaults
3. Preserve old data as backup
Toast: "Updated to new version. Data migrated successfully."
Parse Error 1. Clear corrupted data
2. Load defaults
3. Log error
Toast: "Unable to restore previous session."
Storage Unavailable 1. Detect private/incognito mode
2. Disable auto-save
3. Show warning
Banner: "Auto-save disabled (private mode). Export to save work."

Multi-Tab Synchronization

Problem: Multiple tabs open, each saving independently → conflicts

Solution: Use storage event listener

Recommendation: Start with last-write-wins. Add conflict resolution later if needed.


5. Code Architecture

Folder Structure

/src
  /stores
    /persistence
      constants.ts           # Storage keys, config
      types.ts               # Serialization types
      loader.ts              # Load and validate data
      saver.ts               # Save and serialize data
      middleware.ts          # Zustand middleware for auto-save
      migrations.ts          # Version migration logic (future)
      hooks.ts               # React hooks for persistence features (future)
    graphStore.ts            # Enhanced with persistence
    editorStore.ts           # Enhanced with persistence

Module Responsibilities

constants.ts

  • Storage keys
  • Debounce configuration
  • Current schema version

types.ts

  • ConstellationDocument interface
  • SerializedActor, SerializedRelation interfaces
  • PersistenceError enum

loader.ts

  • Reads from localStorage
  • Validates schema
  • Deserializes data
  • Returns typed ConstellationDocument or null

saver.ts

  • Serializes current store state
  • Writes to localStorage
  • Handles quota errors
  • Updates lastSaved timestamp

middleware.ts

  • Intercepts Zustand state changes
  • Triggers debounced saves
  • Filters what gets persisted

migrations.ts (Phase 3)

  • Version detection
  • Data transformation between versions
  • Backward compatibility

hooks.ts (Phase 3)

  • usePersistence() - Monitor save status
  • useAutoSave() - Manual save trigger
  • useStorageStats() - Storage quota info

6. Migration Strategy

Version Naming Convention

Use semantic versioning: MAJOR.MINOR.PATCH

  • MAJOR: Breaking changes (incompatible schema)
  • MINOR: New fields (backward compatible)
  • PATCH: Bug fixes, no schema changes

Migration Registry

// migrations.ts
type Migration = (old: any) => ConstellationDocument;

const MIGRATIONS: Record<string, Migration> = {
  '0.9.0->1.0.0': (old) => {
    // Example: Rename field
    return {
      ...old,
      graph: {
        ...old.graph,
        nodes: old.graph.actors.map(actor => ({
          ...actor,
          data: { ...actor.data, label: actor.data.name },
        })),
      },
    };
  },
};

7. Implementation Phases

Phase 1: Core Persistence (MVP) IMPLEMENTING NOW

  • Create serialization types
  • Create constants
  • Implement saver.ts with debouncing
  • Implement loader.ts with basic validation
  • Add persistence middleware to graphStore
  • Test save/load cycle

Phase 2: Error Handling

  • Add quota exceeded handling
  • Add corrupted data recovery
  • Add storage unavailable detection
  • Create user-facing error messages

Phase 3: Advanced Features

  • Multi-tab synchronization
  • Migration system
  • Backup rotation
  • Storage stats monitoring

Phase 4: Polish

  • Performance optimization
  • Compression for large graphs
  • Export/import integration
  • User preferences for auto-save behavior

8. Testing Strategy

Test Cases

Manual Tests (Phase 1):

  • Create nodes/edges → Reload page → Verify restored
  • Edit node properties → Reload → Verify persisted
  • Add custom actor types → Reload → Verify persisted
  • Create relations → Reload → Verify persisted

Integration Tests (Phase 2):

  • Save → Clear → Load → Verify state matches
  • Corrupted data → Loads defaults
  • Quota exceeded → Handles gracefully

E2E Tests (Phase 3):

  • Multiple tabs → Changes sync
  • Version migration works

Summary

Key Technical Decisions:

  1. Architecture: Zustand middleware pattern for clean separation
  2. Storage: localStorage with versioned schema
  3. Serialization: Minimal JSON format, exclude React Flow internals
  4. Debouncing: 1s delay, 5s max wait, operation-specific tuning
  5. Validation: Runtime validation on load
  6. Errors: Graceful degradation with user notifications
  7. Multi-tab: Storage event listener with last-write-wins
  8. Migration: Version registry with transformation functions

Current Version: 1.0.0 Current Phase: Phase 1 (MVP Implementation)