Centralized Storage Architecture

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

🏦 Centralized Storage Architecture

Status: 📋 Proposal
Priority: High
Impact: Architecture-wide improvement


🎯 Problem Statement

Currently, storage is handled in an ad-hoc manner:

Example of Current Chaos:

// In flash-persistence
try {
  const storage = inject(FLASH_STORAGE_SERVICE, { optional: true });
  if (!storage) {
    // Fallback to localStorage?
  }
} catch {
  // Use fallback?
}

// In another service
try {
  const storage = inject(GG_STORAGE);
} catch {
  // Different fallback logic
}

✅ Proposed Solution: Storage Registry

Architecture Overview

┌─────────────────────────────────────────────┐
│        Storage Registry Service             │
│  (Single source of truth for all storage)   │
├─────────────────────────────────────────────┤
│                                             │
│  ┌──────────────┐  ┌──────────────┐        │
│  │   Primary    │  │   Fallback   │        │
│  │   GgStorage  │  │  localStorage│        │
│  │  (Dexie/OPFS)│  │              │        │
│  └──────────────┘  └──────────────┘        │
│                                             │
│  ┌──────────────┐  ┌──────────────┐        │
│  │   Mirrors    │  │   Sync       │        │
│  │  (redundancy)│  │  (cloud)     │        │
│  └──────────────┘  └──────────────┘        │
└─────────────────────────────────────────────┘
            │
            ├─→ Flash Persistence
            ├─→ NGXS Storage Plugin
            ├─→ Any other service
            └─→ Direct app usage

Key Concepts

  1. Single Registry: One service that manages all storage backends
  2. Automatic Discovery: Registry discovers available storage on init
  3. Named Backends: Register storage with names ('primary', 'cache', 'temp', etc.)
  4. Fallback Chain: Automatic fallback if primary fails
  5. Runtime Switching: Change backends without restart
  6. Unified API: One consistent interface for all consumers

🔧 Implementation

1. Storage Registry Service

// libs/storage/storage-core/src/lib/storage-registry.service.ts

export interface StorageBackend {
  name: string;
  adapter: GgStorage | IStorageService;
  priority: number;
  capabilities: {
    maxSize: number;
    async: boolean;
    crossTab: boolean;
    persistent: boolean;
  };
  status: 'available' | 'degraded' | 'unavailable';
}

@Injectable({ providedIn: 'root' })
export class StorageRegistryService {
  private backends = new Map<string, StorageBackend>();
  private primaryBackend: string | null = null;
  private fallbackChain: string[] = [];
  
  // Signals for reactivity
  private readonly backendsSignal = signal<Map<string, StorageBackend>>(new Map());
  private readonly statusSignal = signal<'initializing' | 'ready' | 'degraded'>('initializing');
  
  // Public read-only signals
  public readonly backends = this.backendsSignal.asReadonly();
  public readonly status = this.statusSignal.asReadonly();
  
  constructor() {
    this.autoDiscoverBackends();
  }
  
  /**
   * Automatically discover and register available storage backends
   */
  private async autoDiscoverBackends(): Promise<void> {
    console.log('🔍 Storage Registry: Auto-discovering backends...');
    
    // Try to discover GgStorage (primary)
    try {
      const ggStorage = inject(GG_STORAGE, { optional: true });
      if (ggStorage) {
        await this.registerBackend({
          name: 'primary',
          adapter: ggStorage,
          priority: 100,
          capabilities: {
            maxSize: 1024 * 1024 * 1024, // 1GB+
            async: true,
            crossTab: true,
            persistent: true
          },
          status: 'available'
        });
        this.primaryBackend = 'primary';
        console.log('✅ Registered primary storage: GgStorage');
      }
    } catch (error) {
      console.warn('⚠️ GgStorage not available');
    }
    
    // Always register localStorage as fallback
    await this.registerBackend({
      name: 'fallback',
      adapter: this.createLocalStorageAdapter(),
      priority: 10,
      capabilities: {
        maxSize: 10 * 1024 * 1024, // ~10MB
        async: false,
        crossTab: false,
        persistent: true
      },
      status: 'available'
    });
    console.log('✅ Registered fallback storage: localStorage');
    
    // Set fallback chain
    this.fallbackChain = Array.from(this.backends.keys())
      .sort((a, b) => {
        const backendA = this.backends.get(a)!;
        const backendB = this.backends.get(b)!;
        return backendB.priority - backendA.priority;
      });
    
    this.statusSignal.set('ready');
    console.log('🎉 Storage Registry ready:', {
      backends: Array.from(this.backends.keys()),
      primary: this.primaryBackend,
      fallbackChain: this.fallbackChain
    });
  }
  
  /**
   * Register a new storage backend
   */
  async registerBackend(backend: StorageBackend): Promise<void> {
    // Test if backend is actually working
    try {
      await backend.adapter.put('_registry_test', { test: true });
      const result = await backend.adapter.get('_registry_test');
      await backend.adapter.remove('_registry_test');
      
      if (!result) {
        backend.status = 'degraded';
      }
    } catch (error) {
      console.warn(`⚠️ Backend ${backend.name} is not functional:`, error);
      backend.status = 'unavailable';
    }
    
    this.backends.set(backend.name, backend);
    this.backendsSignal.set(new Map(this.backends));
  }
  
  /**
   * Get storage adapter by name, with automatic fallback
   */
  getStorage(name?: string): IStorageService {
    // If no name specified, use primary
    if (!name && this.primaryBackend) {
      name = this.primaryBackend;
    }
    
    // Try requested backend
    if (name) {
      const backend = this.backends.get(name);
      if (backend && backend.status === 'available') {
        return backend.adapter;
      }
    }
    
    // Fallback chain
    for (const backendName of this.fallbackChain) {
      const backend = this.backends.get(backendName);
      if (backend && backend.status === 'available') {
        console.warn(`⚠️ Using fallback storage: ${backendName}`);
        return backend.adapter;
      }
    }
    
    throw new Error('No storage backends available!');
  }
  
  /**
   * Get storage by purpose (better than by name)
   */
  getStorageFor(purpose: 'critical' | 'cache' | 'temp' | 'large' | 'sync'): IStorageService {
    switch (purpose) {
      case 'critical':
        // Use primary with redundancy
        return this.getStorage('primary');
      
      case 'cache':
        // Use fast storage, can lose data
        return this.getStorage('cache') || this.getStorage();
      
      case 'temp':
        // Use any available, prefer fast over reliable
        return this.getStorage('temp') || this.getStorage();
      
      case 'large':
        // Use storage with largest capacity
        return this.getLargestCapacityStorage();
      
      case 'sync':
        // Use storage with cross-tab sync
        return this.getStorageWithCapability('crossTab');
      
      default:
        return this.getStorage();
    }
  }
  
  /**
   * Get all available backends
   */
  getAvailableBackends(): StorageBackend[] {
    return Array.from(this.backends.values())
      .filter(b => b.status === 'available');
  }
  
  /**
   * Get backend status summary
   */
  getStatus(): {
    ready: boolean;
    primary: string | null;
    available: string[];
    degraded: string[];
    unavailable: string[];
  } {
    const backends = Array.from(this.backends.entries());
    
    return {
      ready: this.status() === 'ready',
      primary: this.primaryBackend,
      available: backends.filter(([, b]) => b.status === 'available').map(([name]) => name),
      degraded: backends.filter(([, b]) => b.status === 'degraded').map(([name]) => name),
      unavailable: backends.filter(([, b]) => b.status === 'unavailable').map(([name]) => name)
    };
  }
  
  /**
   * Switch primary backend at runtime
   */
  async switchPrimary(backendName: string): Promise<boolean> {
    const backend = this.backends.get(backendName);
    if (!backend) {
      console.error(`Backend '${backendName}' not found`);
      return false;
    }
    
    if (backend.status !== 'available') {
      console.error(`Backend '${backendName}' is not available`);
      return false;
    }
    
    this.primaryBackend = backendName;
    console.log(`✅ Switched primary storage to: ${backendName}`);
    return true;
  }
  
  private getLargestCapacityStorage(): IStorageService {
    const backends = this.getAvailableBackends()
      .sort((a, b) => b.capabilities.maxSize - a.capabilities.maxSize);
    
    return backends[0]?.adapter || this.getStorage();
  }
  
  private getStorageWithCapability(capability: keyof StorageBackend['capabilities']): IStorageService {
    const backend = this.getAvailableBackends()
      .find(b => b.capabilities[capability]);
    
    return backend?.adapter || this.getStorage();
  }
  
  private createLocalStorageAdapter(): IStorageService {
    // Simple localStorage adapter implementation
    return {
      async get<T>(key: string): Promise<T | null> {
        const item = localStorage.getItem(key);
        return item ? JSON.parse(item) : null;
      },
      async put<T>(key: string, value: T): Promise<boolean> {
        try {
          localStorage.setItem(key, JSON.stringify(value));
          return true;
        } catch {
          return false;
        }
      },
      async remove(key: string): Promise<boolean> {
        localStorage.removeItem(key);
        return true;
      },
      async has(key: string): Promise<boolean> {
        return localStorage.getItem(key) !== null;
      },
      async list(): Promise<string[]> {
        return Object.keys(localStorage);
      }
    };
  }
}

2. Simplified Flash Persistence Integration

// libs/flash-persistence/src/lib/services/flash-state-hydrator.service.ts

@Injectable({ providedIn: 'root' })
export class FlashStateHydratorService {
  private readonly storage: IStorageService;
  private readonly storageRegistry = inject(StorageRegistryService);
  
  constructor() {
    // ✅ SIMPLE: Just ask the registry for appropriate storage
    this.storage = this.storageRegistry.getStorageFor('critical');
    
    console.log('✅ Flash Persistence using storage from registry:', 
      this.storageRegistry.getStatus());
  }
  
  // No more guessing, no more fallback logic, no more injection tokens!
}

3. Global Storage Provider

// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    // ✅ ONE place to configure ALL storage
    provideStorage({
      primary: {
        type: 'gg-storage',
        config: {
          namespace: 'myapp',
          strategy: 'balanced'
        }
      },
      cache: {
        type: 'memory',
        config: {
          maxSize: 50 * 1024 * 1024
        }
      },
      fallback: {
        type: 'localstorage',
        config: {}
      }
    }),
    
    // That's it! Everything else automatically uses the registry
  ]
};

🎉 Benefits

For Developers

✅ No More Guessing: Just inject(StorageRegistryService) and get storage
✅ Consistent API: Same interface everywhere
✅ Better DX: Clear error messages, easy debugging
✅ Type Safety: Full TypeScript support
✅ Reactive: Signals for storage status changes

For the App

✅ Automatic Fallback: No manual fallback logic needed
✅ Runtime Switching: Change storage without restart
✅ Health Monitoring: Know which storage is working
✅ Purpose-Based: Request storage by purpose, not implementation
✅ Centralized Config: One place to configure everything

For Operations

✅ Debugging: See all storage backends and their status
✅ Monitoring: Track storage health in real-time
✅ Flexibility: Easy to add new storage backends
✅ Testing: Mock storage at registry level


📊 Migration Path

Phase 1: Create Registry (Week 1)

Phase 2: Update Consumers (Week 2)

Phase 3: Enhance & Monitor (Week 3)


🎯 Example Usage

Get Storage by Purpose

@Component({})
export class MyComponent {
  private registry = inject(StorageRegistryService);
  
  async saveUserPreferences(prefs: UserPrefs) {
    // Critical data - use primary with redundancy
    const storage = this.registry.getStorageFor('critical');
    await storage.put('user:prefs', prefs);
  }
  
  async cacheImage(url: string, data: Blob) {
    // Cache data - can lose, prefer fast
    const storage = this.registry.getStorageFor('cache');
    await storage.put(`cache:img:${url}`, data);
  }
  
  async saveLargeFile(file: File) {
    // Large data - use storage with most capacity
    const storage = this.registry.getStorageFor('large');
    await storage.put(`files:${file.name}`, file);
  }
}

Monitor Storage Health

@Component({})
export class StorageMonitor {
  private registry = inject(StorageRegistryService);
  
  protected status = this.registry.status;
  protected backends = this.registry.backends;
  
  // Reactive template
  template: `
    <div>
      <h3>Storage Status: {{ status() }}</h3>
      @for (backend of backends() | keyvalue; track backend.key) {
        <div>
          {{ backend.key }}: {{ backend.value.status }}
          <span>{{ backend.value.capabilities.maxSize | fileSize }}</span>
        </div>
      }
    </div>
  `
}

🚀 Next Steps

  1. Review this proposal - Does this architecture make sense?
  2. Implement Phase 1 - Create the registry service
  3. Test with Flash Persistence - Migrate as proof of concept
  4. Roll out gradually - Update other consumers

Status: 📋 Awaiting approval
Priority: High
Estimated Effort: 3 weeks
Impact: Major architectural improvement