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:
- ❌ Each service/library guesses if storage exists
- ❌ Multiple injection points (
FLASH_STORAGE_SERVICE,GG_STORAGE, etc.) - ❌ No centralized registry of available storage backends
- ❌ Difficult to switch storage strategies at runtime
- ❌ No unified configuration
- ❌ Hard to debug storage issues
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
- Single Registry: One service that manages all storage backends
- Automatic Discovery: Registry discovers available storage on init
- Named Backends: Register storage with names (
'primary','cache','temp', etc.) - Fallback Chain: Automatic fallback if primary fails
- Runtime Switching: Change backends without restart
- 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)
- Create
StorageRegistryService - Auto-discovery logic
- Registration API
- Status monitoring
Phase 2: Update Consumers (Week 2)
- Update Flash Persistence to use registry
-
Update NGXS storage plugin(DEPRECATED - Use Flash Persistence instead) - Update direct GgStorage consumers
- Migrate any remaining NGXS usage to Flash Persistence
Phase 3: Enhance & Monitor (Week 3)
- Add health checks
- Add performance metrics
- Add dev tools integration
- Documentation
🎯 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
- Review this proposal - Does this architecture make sense?
- Implement Phase 1 - Create the registry service
- Test with Flash Persistence - Migrate as proof of concept
- Roll out gradually - Update other consumers
Status: 📋 Awaiting approval
Priority: High
Estimated Effort: 3 weeks
Impact: Major architectural improvement