Persistence

QuantaJS provides a powerful persistence system that automatically saves and restores store state across browser sessions, with support for multiple storage adapters, data migrations, and cross-tab synchronization.

Overview

The persistence system consists of:

  • Persistence Manager: Core functionality for managing persisted state
  • Storage Adapters: Different storage backends (localStorage, sessionStorage, IndexedDB)
  • Migration System: Handle schema changes across versions
  • Cross-tab Sync: Synchronize state across multiple browser tabs

Core API

createPersistenceManager

Creates a persistence manager for a store with automatic state saving and loading.

function createPersistenceManager<T extends Record<string, any>>(
  getState: () => T,
  setState: (newState: Partial<T>) => void,
  notifySubscribers: () => void,
  config: PersistenceConfig<T>,
  storeName?: string
): PersistenceManager

Parameters

  • getState: Function that returns the current state
  • setState: Function to update state with new values
  • notifySubscribers: Function to notify store subscribers of changes
  • config: Persistence configuration object
  • storeName: Optional name for the store (used for debugging)

Returns

A PersistenceManager instance with methods for manual persistence control.

Storage Adapters

LocalStorageAdapter

Persists data to browser's localStorage with cross-tab synchronization.

class LocalStorageAdapter implements PersistenceAdapter {
  constructor(key: string)
}

Features:

  • Automatic JSON serialization/deserialization
  • Cross-tab synchronization via storage events
  • Handles quota exceeded errors gracefully

SessionStorageAdapter

Persists data to browser's sessionStorage (cleared when tab closes).

class SessionStorageAdapter implements PersistenceAdapter {
  constructor(key: string)
}

Features:

  • Data persists only for the current browser session
  • Cross-tab synchronization
  • Useful for temporary state that shouldn't persist across sessions

IndexedDBAdapter

Persists data to IndexedDB for larger datasets and better performance.

class IndexedDBAdapter implements PersistenceAdapter {
  constructor(
    key: string,
    dbName?: string,
    storeName?: string,
    version?: number
  )
}

Features:

  • Handles large amounts of data efficiently
  • Asynchronous operations
  • Better performance for complex data structures

Configuration Options

PersistenceConfig

interface PersistenceConfig<T = any> {
  adapter: PersistenceAdapter;
  serialize?: (data: T) => string;
  deserialize?: (data: string) => T;
  migrations?: Record<number, (data: any) => any>;
  version?: number;
  debounceMs?: number;
  include?: Array<keyof T>;
  exclude?: Array<keyof T>;
  transform?: {
    in?: (data: any) => any;
    out?: (data: any) => any;
  };
  onError?: (error: Error, operation: 'read' | 'write' | 'remove') => void;
  validator?: (data: any) => boolean;
}

Configuration Properties

  • adapter: Storage adapter to use (required)
  • serialize: Custom serialization function (defaults to JSON.stringify)
  • deserialize: Custom deserialization function (defaults to JSON.parse)
  • migrations: Version migration functions for schema changes
  • version: Current data schema version (defaults to 1)
  • debounceMs: Debounce delay for save operations (defaults to 300ms)
  • include: Array of state properties to persist (if not specified, all properties are persisted)
  • exclude: Array of state properties to exclude from persistence
  • transform: Data transformation functions for input/output
  • onError: Error handler for persistence operations
  • validator: Function to validate data before saving/after loading

Migration System

MigrationManager

Handles schema changes across different versions of your data.

class MigrationManager<T = any> {
  constructor(migrations: MigrationDefinition<T>[] = [])
  
  addMigration(version: number, migrate: MigrationFunction<T>): void
  removeMigration(version: number): void
  getVersions(): number[]
  hasMigration(version: number): boolean
  getHighestVersion(): number
  migrate(data: any, fromVersion: number, toVersion: number): any
  validateMigrations(): { valid: boolean; errors: string[] }
}

Common Migration Patterns

import { CommonMigrations } from '@quantajs/core';

const migrations = {
  2: CommonMigrations.addProperty('settings', { theme: 'light' }),
  3: CommonMigrations.renameProperty('user', 'currentUser'),
  4: CommonMigrations.transformProperty('todos', (todos) => 
    todos.map(todo => ({ ...todo, priority: 'medium' }))
  ),
  5: CommonMigrations.removeProperty('deprecatedField'),
};

Persistence Manager

PersistenceManager

Interface for controlling persistence operations.

interface PersistenceManager {
  save(): Promise<void>;
  load(): Promise<void>;
  clear(): Promise<void>;
  getAdapter(): PersistenceAdapter;
  isRehydrated(): boolean;
}

Methods

  • save(): Manually trigger a save operation
  • load(): Manually trigger a load operation
  • clear(): Remove all persisted data
  • getAdapter(): Get the underlying storage adapter
  • isRehydrated(): Check if initial data has been loaded

Basic Usage

Simple Persistence

import { createStore, LocalStorageAdapter } from '@quantajs/core';

const store = createStore('user', {
  state: () => ({
    name: '',
    email: '',
    preferences: { theme: 'light' }
  }),
  persist: {
    adapter: new LocalStorageAdapter('user-store'),
    version: 1,
    debounceMs: 500
  }
});

Advanced Persistence with Migrations

import { createStore, LocalStorageAdapter, CommonMigrations } from '@quantajs/core';

const store = createStore('app', {
  state: () => ({
    user: null,
    settings: { theme: 'light', language: 'en' },
    todos: []
  }),
  persist: {
    adapter: new LocalStorageAdapter('app-store'),
    version: 3,
    debounceMs: 300,
    include: ['user', 'settings'], // Only persist user and settings
    exclude: ['todos'], // Explicitly exclude todos
    migrations: {
      2: CommonMigrations.addProperty('settings', { language: 'en' }),
      3: CommonMigrations.renameProperty('theme', 'appTheme')
    },
    onError: (error, operation) => {
      console.error(`Persistence ${operation} failed:`, error);
    }
  }
});

Custom Serialization

const store = createStore('complex', {
  state: () => ({
    dates: [new Date()],
    functions: [() => console.log('test')]
  }),
  persist: {
    adapter: new LocalStorageAdapter('complex-store'),
    serialize: (data) => {
      // Custom serialization to handle Date objects
      return JSON.stringify(data, (key, value) => {
        if (value instanceof Date) {
          return { __type: 'Date', value: value.toISOString() };
        }
        return value;
      });
    },
    deserialize: (data) => {
      // Custom deserialization to restore Date objects
      return JSON.parse(data, (key, value) => {
        if (value && value.__type === 'Date') {
          return new Date(value.value);
        }
        return value;
      });
    }
  }
});

Data Validation

const store = createStore('validated', {
  state: () => ({
    count: 0,
    name: ''
  }),
  persist: {
    adapter: new LocalStorageAdapter('validated-store'),
    validator: (data) => {
      // Ensure count is a number and name is a string
      return typeof data.count === 'number' && 
             typeof data.name === 'string' &&
             data.count >= 0;
    },
    onError: (error, operation) => {
      if (operation === 'read') {
        // If validation fails on read, reset to defaults
        store.$reset();
      }
    }
  }
});

Cross-tab Synchronization

All storage adapters support cross-tab synchronization automatically. When data changes in one tab, other tabs are automatically updated:

// Tab 1
store.name = 'John';

// Tab 2 automatically receives the update
console.log(store.name); // 'John'

Error Handling

The persistence system provides comprehensive error handling:

const store = createStore('robust', {
  state: () => ({ data: [] }),
  persist: {
    adapter: new LocalStorageAdapter('robust-store'),
    onError: (error, operation) => {
      switch (operation) {
        case 'read':
          console.warn('Failed to load persisted state, using defaults');
          break;
        case 'write':
          console.error('Failed to save state:', error);
          // Optionally show user notification
          break;
        case 'remove':
          console.error('Failed to clear persisted data:', error);
          break;
      }
    }
  }
});

Performance Considerations

Debouncing

The persistence system automatically debounces save operations to prevent excessive writes:

const store = createStore('performance', {
  state: () => ({ counter: 0 }),
  persist: {
    adapter: new LocalStorageAdapter('performance-store'),
    debounceMs: 1000 // Save only after 1 second of inactivity
  }
});

// Rapid changes will be batched
for (let i = 0; i < 100; i++) {
  store.counter++;
}
// Only one save operation will occur after 1 second

Selective Persistence

Only persist necessary data to improve performance:

const store = createStore('selective', {
  state: () => ({
    user: null,
    temporaryData: [],
    cache: new Map()
  }),
  persist: {
    adapter: new LocalStorageAdapter('selective-store'),
    include: ['user'], // Only persist user data
    exclude: ['temporaryData', 'cache'] // Explicitly exclude large objects
  }
});

Learn More