Skip to main content

Storage Management

Time Capsule uses a layered storage approach with IndexedDB as the primary storage mechanism, localStorage as a fallback, and an in-memory cache for performance.

Storage Architecture

SettingsManager Implementation

The SettingsManager class provides a unified interface for all configuration and state management.

Singleton Pattern

class SettingsManager {
  private static instance: SettingsManager;
  private settings: SystemSettings;
  private readonly CURRENT_VERSION = '1.1.1';

  private constructor() {
    this.settings = this.getDefaultSettings();
    this.load();
    this.checkVersion();
  }

  public static getInstance(): SettingsManager {
    if (!SettingsManager.instance) {
      SettingsManager.instance = new SettingsManager();
    }
    return SettingsManager.instance;
  }
}

export const settingsManager = SettingsManager.getInstance();

Settings Structure

Settings are organized into logical sections:
export interface SystemSettings {
  theme: {
    colors: Record<string, string>;
    fonts: Record<string, string>;
  };
  mouse: Record<string, any>;
  keyboard: Record<string, any>;
  beep: Record<string, any>;
  session: {
    windows: Record<string, {
      top: string;
      left: string;
      display: string;
      maximized: boolean;
    }>;
  };
  desktop: Record<string, any>;
}

Version Management

Settings are versioned to handle schema changes and cache invalidation:
private checkVersion(): void {
  const lastVersion = storageAdapter.getItemSync(
    `${STORAGE_KEY}-version`
  );
  
  if (lastVersion !== this.CURRENT_VERSION) {
    logger.log(
      `[SettingsManager] Version mismatch ` +
      `(${lastVersion} vs ${this.CURRENT_VERSION}). ` +
      `Resetting cache...`
    );
    this.resetToDefaults();
    storageAdapter.setItemSync(
      `${STORAGE_KEY}-version`, 
      this.CURRENT_VERSION
    );
  }
}
When the version changes, all cached settings are cleared and reset to defaults. This prevents issues from schema changes.

Storage Adapter

The StorageAdapter provides a unified API abstracting the underlying storage mechanism.

Synchronous Operations

private load(): void {
  try {
    const saved = storageAdapter.getItemSync(STORAGE_KEY);
    if (saved) {
      this.settings = JSON.parse(saved);
      logger.log('[SettingsManager] Unified settings loaded.');
    } else {
      this.migrateLegacySettings();
    }
  } catch (e) {
    console.error('[SettingsManager] Failed to load:', e);
  }
}

public save(): void {
  try {
    storageAdapter.setItemSync(
      STORAGE_KEY, 
      JSON.stringify(this.settings)
    );
  } catch (e) {
    console.error('[SettingsManager] Failed to save:', e);
  }
}

Legacy Migration

Old settings stored in fragmented keys are automatically migrated:
private migrateLegacySettings(): void {
  logger.log(
    '[SettingsManager] Migrating from legacy settings...'
  );

  // Migrate Style/Theme (cde-styles)
  const oldStyles = storageAdapter.getItemSync('cde-styles');
  if (oldStyles) {
    const parsed = JSON.parse(oldStyles);
    this.settings.theme.colors = parsed.colors || {};
    this.settings.theme.fonts = parsed.fonts || {};
  }

  // Migrate Mouse
  const oldMouse = storageAdapter.getItemSync(
    'cde-mouse-settings'
  );
  if (oldMouse) {
    this.settings.mouse = JSON.parse(oldMouse);
  }

  // Migrate Keyboard
  const oldKeyboard = storageAdapter.getItemSync(
    'cde-keyboard-settings'
  );
  if (oldKeyboard) {
    this.settings.keyboard = JSON.parse(oldKeyboard);
  }

  // Migrate Beep
  const oldBeep = storageAdapter.getItemSync(
    'cde-beep-settings'
  );
  if (oldBeep) {
    this.settings.beep = JSON.parse(oldBeep);
  }

  this.save();
  logger.log('[SettingsManager] Migration completed.');
}

Section Management

Settings are accessed and updated by section:
public setSection(
  section: keyof SystemSettings, 
  data: any
): void {
  (this.settings as any)[section] = data;
  this.save();
}

public getSection(section: keyof SystemSettings): any {
  return this.settings[section];
}

public getAll(): SystemSettings {
  return this.settings;
}

Window Session Persistence

Window positions and states are automatically saved:
public updateWindowSession(id: string, data: any): void {
  this.settings.session.windows[id] = {
    ...this.settings.session.windows[id],
    ...data
  };
  this.save();
}

IndexedDB Usage

While SettingsManager currently uses synchronous localStorage operations via the adapter, the architecture supports IndexedDB for future enhancements.

Planned IndexedDB Schema

const DB_NAME = 'cde-time-capsule';
const DB_VERSION = 1;

const STORES = {
  SETTINGS: 'settings',    // User preferences
  SESSION: 'session',      // Window state
  FILESYSTEM: 'filesystem', // VFS data (future)
  CACHE: 'cache',          // Temporary data
};

Cache Management

Memory Cache

SettingsManager maintains an in-memory cache for fast access:
class SettingsManager {
  private settings: SystemSettings; // Memory cache

  // Direct memory access (fast)
  public getSection(section: keyof SystemSettings): any {
    return this.settings[section];
  }

  // Write to memory + storage
  public setSection(section: keyof SystemSettings, data: any): void {
    this.settings[section] = data; // Update cache
    this.save();                    // Persist to storage
  }
}

XPM Backdrop Cache

Rendered backdrop images are cached to avoid re-parsing expensive XPM files:
// Clear cache when theme colors change
public saveColor(): void {
  const theme = settingsManager.getSection('theme');
  theme.colors = this.theme.styles;
  settingsManager.setSection('theme', theme);
  
  // Invalidate XPM cache
  this.backdrop.clearCache();
  this.backdrop.apply();
}

Performance Considerations

Synchronous Operations

SettingsManager uses synchronous localStorage operations for simplicity and reliability:
Synchronous storage operations are acceptable here because:
  1. Settings data is small (typically < 50KB)
  2. Operations are infrequent (user actions only)
  3. Blocking UI briefly is better than race conditions
  4. localStorage is much faster than IndexedDB for small data

Batched Updates

Avoid calling save() in tight loops:
// DON'T: Save on every value change
for (const [key, value] of Object.entries(colors)) {
  settings.theme.colors[key] = value;
  settingsManager.save(); // ❌ Multiple disk writes
}

Selective Section Updates

Only update the section that changed:
// Get section reference
const theme = settingsManager.getSection('theme');

// Modify section
theme.colors['--window-color'] = '#4d648d';

// Save entire settings (includes updated theme)
settingsManager.setSection('theme', theme);

Error Handling

Graceful degradation ensures the application continues working even if storage fails:
private load(): void {
  try {
    const saved = storageAdapter.getItemSync(STORAGE_KEY);
    if (saved) {
      this.settings = JSON.parse(saved);
    }
  } catch (e) {
    console.error('[SettingsManager] Load failed:', e);
    // Continue with default settings
  }
}

public save(): void {
  try {
    storageAdapter.setItemSync(
      STORAGE_KEY,
      JSON.stringify(this.settings)
    );
  } catch (e) {
    console.error('[SettingsManager] Save failed:', e);
    // User changes will be lost on refresh, but app continues
  }
}

Storage Quota

Check Available Space

async function getStorageEstimate() {
  if ('storage' in navigator && 
      'estimate' in navigator.storage) {
    const estimate = await navigator.storage.estimate();
    return {
      usage: estimate.usage || 0,
      quota: estimate.quota || 0,
      percentage: (
        (estimate.usage || 0) / (estimate.quota || 1)
      ) * 100
    };
  }
  return null;
}

Handle Quota Exceeded

try {
  storageAdapter.setItemSync(key, value);
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    // Clear old cache entries
    console.warn('Storage quota exceeded, clearing cache');
    // Implement cache cleanup strategy
  }
}

Best Practices

Always access settings by section rather than modifying the entire settings object:
// Good
const theme = settingsManager.getSection('theme');
theme.colors['--window-color'] = '#4d648d';
settingsManager.setSection('theme', theme);

// Bad
const all = settingsManager.getAll();
all.theme.colors['--window-color'] = '#4d648d';
// No automatic save!
Always provide defaults for potentially missing data:
const theme = settingsManager.getSection('theme');
const colors = theme?.colors || {};
const windowColor = colors['--window-color'] || '#4d648d';
When making breaking changes to settings structure, increment the version:
private readonly CURRENT_VERSION = '1.2.0'; // Changed from 1.1.1
This triggers automatic cache reset on next load.

Testing Storage

Manual Testing in Console

// Check current settings
console.log(settingsManager.getAll());

// Modify theme colors
const theme = settingsManager.getSection('theme');
theme.colors['--window-color'] = '#ff0000';
settingsManager.setSection('theme', theme);

// Verify persistence (refresh page and check)
location.reload();
console.log(settingsManager.getSection('theme').colors);

// Check localStorage directly
console.log(localStorage.getItem('cde-system-settings'));

Clear All Settings

// In browser console
localStorage.clear();
location.reload();

// Or programmatically
settingsManager.resetToDefaults();