How to Implement Storage System
Complete guide to implementing our intelligent storage system across the NX monorepo
How to Implement Our Intelligent Storage System
🎯 Overview
This guide provides a complete step-by-step implementation of our intelligent storage system across your Angular applications. Our storage system provides automatic adapter selection, smart redundancy, and zero-configuration intelligence.
Session 166.3 Update: For features that need both local persistence AND backend sync (chat, preferences, drafts, etc.), use the new BackendSyncEngine + syncToBackend(). It provides:
autoFlashState()for instant IndexedDB persistenceBackendSyncEnginewithSyncDescriptor<T>for fire-and-forget backend syncsyncToBackend()— one-liner to watch a signal and auto-sync changes- Persistent retry queue (survives page refresh, drains on reconnect)
- Two-signal pattern for race-condition-free backend data merging
Quick example:
const prefs = autoFlashState('user-prefs', { theme: 'light' }); syncToBackend(prefs, { descriptorKey: 'user-prefs', debounceMs: 1000 }); // That's it — instant local + durable backend with retry resilience
For features that DON'T need backend sync (local-only UI state, caches), the patterns below remain the best approach.
🚀 RECOMMENDED: Repository Pattern Approach ⭐ NEW!
Why This is Now Our Best Practice
- ✅ Complete CRUD: findById, create, update, delete built-in
- ✅ Advanced Querying: Filter, paginate, sort out of the box
- ✅ Batch Operations: createMany, updateMany, deleteMany (10x faster!)
- ✅ L1 + L2 Caching: Automatic dual-layer caching
- ✅ Cache Statistics: Signals for hits/misses/hitRate
- ✅ Event Publishing: EntityCreated/Updated/Deleted events
- ✅ RxJS-First: All Observables, no Promises
- ✅ Offline-First: Background sync to MongoDB
- ✅ Type-Safe: Full TypeScript support
- ✅ Tested: 16/16 tests passing
Quick Start (Repository Pattern) ⭐
Step 1: Define Your Entity
// models/universe.model.ts
import { BaseEntity } from '@gograb-space/storage-system';
export interface Universe extends BaseEntity {
id: string;
name: string;
description?: string;
spaceId: string;
status: 'active' | 'archived';
createdAt: number;
updatedAt: number;
}
Step 2: Create Repository
// repositories/universe.repository.ts
import { Injectable, inject } from '@angular/core';
import { BaseRepository } from '@gograb-space/storage-system';
import { GG_STORAGE } from '@gograb-space/storage-system/angular';
import { Universe } from '../models/universe.model';
@Injectable({ providedIn: 'root' })
export class UniverseRepository extends BaseRepository<Universe> {
protected readonly entityName = 'Universe';
protected readonly baseUrl = '/api/universes';
constructor() {
const storage = inject(GG_STORAGE);
super(storage, {
cache: {
maxEntries: 50, // L1 cache: 50 universes
ttl: 600000, // 10 minutes
enableStats: true
},
enableEvents: true
});
}
}
Step 3: Use in Service
// services/space.service.ts
@Injectable({ providedIn: 'root' })
export class SpaceService {
private universeRepo = inject(UniverseRepository);
// ✅ Complete CRUD + caching automatically!
async loadUniverse(id: string) {
return firstValueFrom(this.universeRepo.findById(id));
}
async createUniverse(data: Partial<Universe>) {
return firstValueFrom(this.universeRepo.create(data));
}
// ✅ NEW: Advanced querying
async getActiveUniverses(page: number = 1) {
return firstValueFrom(
this.universeRepo.findAll({
filter: { status: 'active' },
pagination: { page, pageSize: 10 },
sort: { field: 'createdAt', direction: 'desc' }
})
);
}
// ✅ NEW: Batch operations (10x faster!)
async createMultiple(universes: Partial<Universe>[]) {
return firstValueFrom(this.universeRepo.createMany(universes));
}
// ✅ NEW: Utility methods
async countActive() {
return firstValueFrom(this.universeRepo.count({ status: 'active' }));
}
}
Step 4: Monitor Cache Performance
// Check cache statistics
const stats = this.universeRepo.getCacheStats();
console.log('Cache hit rate:', stats.hitRate, '%');
console.log('L1 cache size:', stats.size, 'entries');
// Or use signals in template
@Component({
template: `
<div class="stats">
Cache Hit Rate: {{ universeRepo.cacheHitRate() }}%
Cache Size: {{ universeRepo.cacheSize() }} entries
</div>
`
})
Performance: First read 15-20ms (L2), second read 1-2ms (L1 cache hit!) ⚡
🔄 Alternative: HTTP + Mock API Approach
Use when you need realistic API simulation for development:
Why This is Our Preferred Pattern
- ✅ Solid Foundation: All data flows through our intelligent storage system
- ✅ API-Ready: Zero refactoring when real API launches
- ✅ Performance: One HTTP call to cache, then storage-speed access
- ✅ Realistic: Network simulation, validation, error handling
Quick Start (HTTP + Mock API)
Step 1: Configure Mock API with Intelligent Storage
// app.config.ts
import { mockHttpInterceptor, MockInterceptorUtilsService } from '@mock-api';
import { provideAppInitializer } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([mockHttpInterceptor])),
// Modern app initializer pattern
provideAppInitializer(() => {
const utils = inject(MockInterceptorUtilsService);
return () => utils.configureMockInterceptor({
enabled: !environment.production,
persistence: {
enabled: true,
storage: 'intelligent', // 🧠 Uses our intelligent storage!
namespace: 'your-app-api',
ttl: 3600000, // 1 hour cache
autoCleanup: true
}
}, {
// Your API mock definitions
'GET /api/data': (req) => ({ status: 200, data: mockData }),
'POST /api/data': (req) => ({ status: 201, data: req.body })
});
})
]
};
Step 2: Create Services Using HTTP
// your-feature.service.ts
@Injectable({ providedIn: 'root' })
export class YourFeatureService {
private http = inject(HttpClient);
// ✅ RECOMMENDED: HTTP calls automatically cached by intelligent storage
getData(): Observable<any[]> {
return this.http.get<any[]>('/api/data');
}
saveData(data: any): Observable<any> {
return this.http.post<any>('/api/data', data);
}
}
Step 3: Use in Components
// your-component.ts
@Component({
selector: 'app-your-component',
template: `
<div>
@if (data$ | async; as data) {
@for (item of data; track item.id) {
<p>{{ item.name }}</p>
}
}
<button (click)="saveData()">Save</button>
</div>
`
})
export class YourComponent {
private service = inject(YourFeatureService);
data$ = this.service.getData(); // Cached by intelligent storage
saveData() {
this.service.saveData({ name: 'Test' }).subscribe();
}
}
🔄 Alternative: Direct Storage Access
Use only for legacy features or special cases where HTTP simulation isn't needed:
Step 1: Add Storage Provider
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideGgStorage } from 'libs/storage-angular/src/lib/providers';
import { GgStorage } from 'libs/storage-core/src/lib/gg-storage';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
// ✅ CORRECT: Provide real storage with factory function
...provideGgStorage({
namespace: 'my-app', // Unique namespace for your app
strategy: 'balanced', // 'speed' | 'balanced' | 'durable'
cache: {
maxItems: 1000, // Memory cache size
ttlMs: 300_000 // 5 minute cache TTL
},
performance: {
adaptiveStrategy: true, // Enable intelligent adapter selection
redundancy: true, // Enable smart data mirroring
monitoring: true, // Enable performance tracking
batchProcessing: true, // Enable operation batching
microCacheBudget: 100 * 1024 // 100KB micro-cache budget
}
}, (config) => new GgStorage(config)), // 🔑 CRITICAL: Real storage factory
// ... other providers
]
};
Step 2: Use Storage Service
// my-feature.service.ts
import { Injectable, inject } from '@angular/core';
import { StorageService } from 'libs/storage-angular/src/lib/storage.service';
@Injectable({ providedIn: 'root' })
export class MyFeatureService {
private readonly storage = inject(StorageService);
// ✅ Simple, intelligent storage usage
async saveUserData(userData: any): Promise<boolean> {
// System automatically chooses optimal storage location and strategy
return await this.storage.put('user/profile', userData);
}
async getUserData(): Promise<any> {
return await this.storage.get('user/profile');
}
// ✅ Reactive storage with signals
userSettings = this.storage.asSignal<UserSettings>('user/settings', {
theme: 'light',
language: 'en'
}).value;
}
Step 3: Use in Components
// my-component.ts
import { Component, inject } from '@angular/core';
import { StorageService } from 'libs/storage-angular/src/lib/storage.service';
@Component({
selector: 'app-my-component',
template: `
<div>
<p>User: {{ userData()?.name }}</p>
<button (click)="saveData()">Save</button>
</div>
`
})
export class MyComponent {
private readonly storage = inject(StorageService);
// ✅ Reactive storage with automatic updates
userData = this.storage.asSignal<any>('user/profile', null).value;
async saveData() {
// System handles everything intelligently
await this.storage.put('user/profile', { name: 'John', email: '[email protected]' });
}
}
📊 All Available Storage Types (Mock API)
When using HTTP + Mock API approach, you can choose from 5 storage types for persistence:
1. 'intelligent' (RECOMMENDED) 🧠
persistence: {
enabled: true,
storage: 'intelligent', // Uses your libs/storage-core/src/lib/gg-storage intelligent system
namespace: 'api-cache',
ttl: 3600000 // 1 hour cache
}
What it does: Routes to optimal adapter (OPFS → Dexie → localStorage) with automatic redundancy, smart caching, and performance monitoring
Performance: First access 15-50ms, cached access **0.2ms**
When to use: 99% of cases - All production features, development with real data
2. 'indexeddb' (Legacy Direct) 💾
persistence: {
storage: 'indexeddb',
dbName: 'mock-api-db',
storeName: 'responses'
}
What it does: Direct IndexedDB access using Dexie, no intelligence
Performance: ~20-100ms first access, ~2-10ms cached
When to use: Legacy compatibility during migration, direct IndexedDB control needed
3. 'localstorage' (Legacy Direct) 🗂️
persistence: {
storage: 'localstorage'
}
What it does: Direct localStorage access, limited to ~5-10MB
Performance: ~1-5ms (consistent but limited space)
When to use: Testing predictable localStorage behavior, debugging storage issues
4. 'memory' (Temporary) 🔄
persistence: {
storage: 'memory' // Default when no storage specified
}
What it does: In-memory only, data lost on refresh
Performance: ~0.1ms (fastest possible)
When to use: Unit testing, demo presentations, performance benchmarking
5. 'storage-core' (Alias) 📦
persistence: {
storage: 'storage-core' // Same as 'intelligent'
}
What it does: Exact same as 'intelligent' (just an alias)
📈 Performance Comparison Table
| Storage Type | First Access | Cached Access | Storage Limit | Intelligence | Persistence |
|---|---|---|---|---|---|
intelligent |
15-50ms | 0.2ms | 20GB+* | ✅ Full | ✅ Yes |
indexeddb |
20-100ms | 2-10ms | ~1GB | ❌ None | ✅ Yes |
localstorage |
1-5ms | 1-5ms | ~5-10MB | ❌ None | ✅ Yes |
memory |
0.1ms | 0.1ms | RAM only | ❌ None | ❌ No |
storage-core |
15-50ms | 0.2ms | 20GB+* | ✅ Full | ✅ Yes |
**20GB+ = OPFS (20GB) + IndexedDB (1GB) + localStorage (10MB) combined intelligently
🎯 When NOT to Use Intelligent Storage
❌ Unit Testing - Use 'memory'
// Unit tests need clean state per test
const testConfig = {
persistence: {
storage: 'memory', // ← Clean state, no side effects
ttl: 0 // No expiration needed
}
};
beforeEach(() => {
configureMockInterceptor(testConfig, testMocks);
});
❌ Demo Presentations - Use 'memory'
// Demos need fresh data each session
const demoConfig = {
persistence: {
storage: 'memory', // ← Fresh data every demo
ttl: 3600000 // 1 hour demo session
},
delay: [200, 500] // Realistic delays for demo
};
❌ Performance Benchmarking - Use 'memory'
// Isolate network performance from storage performance
const benchmarkConfig = {
persistence: {
storage: 'memory', // ← No I/O overhead
ttl: 0
},
delay: 0 // No artificial delays
};
❌ Storage Debugging - Use 'localstorage' or 'indexeddb'
// Force specific adapter to isolate issues
const debugConfig = {
persistence: {
storage: 'localstorage', // ← Force localStorage for debugging
autoCleanup: false // Keep data for inspection
}
};
❌ Legacy Migration - Use 'indexeddb'
// Temporary during migration from old IndexedDB code
const migrationConfig = {
persistence: {
storage: 'indexeddb', // ← Match old database structure
dbName: 'legacy-app-db',
storeName: 'legacy-data'
}
};
🧠 Storage Strategies Explained (Direct Storage Only)
Note: These strategies only apply when using direct storage access (not HTTP + Mock API). For Mock API, use storage type selection above.
Speed Strategy ('speed')
Best for: Performance-critical applications, real-time apps
provideGgStorage({
strategy: 'speed',
namespace: 'performance-app'
}, (config) => new GgStorage(config))
- Behavior: Micro-cache + queued OPFS writes for maximum performance
- Use Case: Games, real-time collaboration, high-frequency updates
Balanced Strategy ('balanced') ⭐ Recommended
Best for: General-purpose applications
provideGgStorage({
strategy: 'balanced',
namespace: 'general-app'
}, (config) => new GgStorage(config))
- Behavior: Optimal balance of speed, reliability, and storage efficiency
- Use Case: Business apps, dashboards, content management
Durable Strategy ('durable')
Best for: Critical data applications
provideGgStorage({
strategy: 'durable',
namespace: 'critical-app'
}, (config) => new GgStorage(config))
- Behavior: Synchronous writes with triple redundancy for maximum data safety
- Use Case: Financial apps, medical records, legal documents
📊 Configuration Options
Basic Configuration
...provideGgStorage({
namespace: 'my-app', // REQUIRED: Unique app identifier
strategy: 'balanced', // Storage strategy
cache: {
maxItems: 500, // Memory cache limit
ttlMs: 300_000 // Cache TTL in milliseconds
}
}, (config) => new GgStorage(config))
Advanced Configuration
...provideGgStorage({
namespace: 'advanced-app',
strategy: 'speed',
adapterPreference: 'opfs', // Preferred adapter when available
cache: {
maxItems: 2000,
ttlMs: 600_000 // 10 minute cache
},
performance: {
adaptiveStrategy: true, // Enable AI-powered decisions
redundancy: true, // Enable smart data mirroring
monitoring: true, // Enable performance tracking
batchProcessing: true, // Enable operation batching
batchSize: 50, // Batch size for operations
batchDelay: 100, // Delay between batches (ms)
microCacheBudget: 200 * 1024 // 200KB micro-cache budget
},
sync: {
adapter: cloudSyncAdapter // Optional cloud synchronization
}
}, (config) => new GgStorage(config))
Cloud Sync Configuration
import { FirestoreSyncAdapter } from 'libs/storage-sync/src/index';
const cloudSyncAdapter = new FirestoreSyncAdapter({
projectId: "your-project-id",
apiKey: "your-api-key",
authDomain: "your-project.firebaseapp.com",
databaseId: 'production-db',
collection: 'user-data'
});
...provideGgStorage({
namespace: 'cloud-app',
strategy: 'balanced',
sync: { adapter: cloudSyncAdapter }
}, (config) => new GgStorage(config))
🎯 Key Patterns for Intelligence
Use Semantic Keys
// ✅ GOOD: Descriptive keys help intelligent categorization
await storage.put('user/preferences/theme', themeData); // → Critical: Redundant
await storage.put('auth/session/token', sessionToken); // → Critical: Triple redundancy
await storage.put('cache/api/users', usersData); // → Cache: IndexedDB
await storage.put('temp/upload/file-123', fileData); // → Temp: OPFS performance
await storage.put('settings/dashboard', dashboardConfig); // → User-pref: Redundant
// ❌ AVOID: Generic keys that don't convey meaning
await storage.put('data1', themeData); // System can't optimize
await storage.put('token', sessionToken); // No intelligent routing
Leverage Intelligent Features
// ✅ Get storage insights and diagnostics
const capabilities = storage.getCapabilities();
console.log('Available adapters:', capabilities);
const performance = storage.getPerformanceStats();
if (performance) {
console.log('Cache hit rate:', performance.cacheHitRate);
console.log('Average operation time:', performance.avgDuration);
}
// ✅ Use micro-cache statistics for optimization
const microStats = storage.getMicroCacheStats();
console.log('Micro-cache usage:', microStats.size, '/', microStats.budget);
🚫 Critical Mistakes to Avoid
❌ Mistake 1: Using Mock Storage Factory
// ❌ WRONG: This creates a mock storage that doesn't save anything!
...provideGgStorage({
namespace: 'my-app',
strategy: 'balanced'
})
// Missing factory function → Uses mock implementation
// ✅ CORRECT: Always provide real storage factory
...provideGgStorage({
namespace: 'my-app',
strategy: 'balanced'
}, (config) => new GgStorage(config)) // Real storage implementation
❌ Mistake 2: Manual Adapter Switching
// ❌ WRONG: Manual switching interferes with intelligence
storage.switchAdapter('localStorage');
storage.switchNamespace('different-app');
// ✅ CORRECT: Trust the intelligent system
// System automatically chooses optimal adapters and handles failures
await storage.put('user/data', userData); // Intelligence handles everything
❌ Mistake 3: Multiple Storage Configurations
// ❌ WRONG: Creating multiple storage instances
export const appConfig: ApplicationConfig = {
providers: [
...provideGgStorage({ namespace: 'app1' }, factory),
...provideGgStorage({ namespace: 'app2' }, factory), // Conflict!
...provideMyFeatureStorage({ namespace: 'feature' }) // Different storage!
]
};
// ✅ CORRECT: Single storage instance with semantic keys
export const appConfig: ApplicationConfig = {
providers: [
...provideGgStorage({ namespace: 'unified-app' }, factory),
// Use semantic keys to separate concerns:
// 'feature1/data', 'feature2/data', etc.
]
};
❌ Mistake 4: Blocking Storage Operations
// ❌ WRONG: Synchronous blocking operations
const data = storage.get('user/data'); // Returns Promise, not data!
// ✅ CORRECT: Proper async/await patterns
const data = await storage.get('user/data');
// ✅ BETTER: Use reactive signals for UI
const userData = storage.asSignal('user/data', null).value;
🔍 Debugging and Monitoring
Enable Development Logging
// For development environments
...provideGgStorage({
namespace: 'dev-app',
strategy: 'balanced',
performance: {
monitoring: true, // Enable detailed performance logging
adaptiveStrategy: true
}
}, (config) => new GgStorage(config))
Check Storage Health
// Get storage diagnostics
const capabilities = storage.getCapabilities();
console.log('Storage capabilities:', capabilities);
// Check current strategy
console.log('Current strategy:', storage.getStrategy());
// Get performance statistics
const stats = storage.getPerformanceStats();
if (stats) {
console.log('Performance metrics:', stats);
}
// Check micro-cache usage
const microStats = storage.getMicroCacheStats();
console.log('Micro-cache stats:', microStats);
Common Debug Logs
// These log patterns indicate healthy operation:
// ✅ "🧠 PUT Decision for "user/data": 💾 OPFS storage..."
// ✅ "⚡ GET "user/data" from memory in 0.10ms"
// ✅ "📦 Micro-cached: user/settings"
// These patterns indicate problems:
// ❌ "All keys found: 0 []" (mock storage or namespace issue)
// ❌ "PUT "key" completed in 0ms, success: true" (mock storage)
// ❌ "Failed to save" followed by "success: true" (mock storage)
📁 File Organization
Recommended Project Structure
my-nx-app/
├── src/
│ ├── app/
│ │ ├── app.config.ts ← Storage configuration here
│ │ ├── features/
│ │ │ ├── user-management/
│ │ │ │ └── user.service.ts ← Use 'user/*' keys
│ │ │ └── dashboard/
│ │ │ └── dashboard.service.ts ← Use 'dashboard/*' keys
│ │ └── shared/
│ │ └── services/
│ │ └── app-storage.service.ts ← Central storage service
Storage Service Patterns
// ✅ Feature-specific storage service
@Injectable({ providedIn: 'root' })
export class UserManagementStorageService {
private readonly storage = inject(StorageService);
// Use semantic key patterns
async saveProfile(userId: string, profile: UserProfile) {
return await this.storage.put(`user/${userId}/profile`, profile);
}
async savePreferences(userId: string, prefs: UserPreferences) {
return await this.storage.put(`user/${userId}/preferences`, prefs);
}
// Reactive data access
getUserProfile(userId: string) {
return this.storage.asSignal<UserProfile>(`user/${userId}/profile`, null).value;
}
}
🔧 Advanced Usage Patterns
Reactive Data Patterns
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<h1>Welcome {{ user()?.name }}</h1>
<div class="settings">
Theme: {{ userSettings()?.theme }}
</div>
</div>
`
})
export class DashboardComponent {
private storage = inject(StorageService);
// ✅ Reactive signals automatically update UI
user = this.storage.asSignal<User>('user/profile', null).value;
userSettings = this.storage.asSignal<Settings>('user/settings', {
theme: 'light',
language: 'en'
}).value;
async updateTheme(newTheme: string) {
const currentSettings = this.userSettings();
if (currentSettings) {
await this.storage.put('user/settings', {
...currentSettings,
theme: newTheme
});
// UI automatically updates via signals!
}
}
}
Batch Operations
// ✅ System automatically batches for efficiency
async saveBulkData(items: DataItem[]) {
const promises = items.map((item, index) =>
this.storage.put(`bulk/data/${index}`, item)
);
await Promise.all(promises);
// System automatically batches these operations for optimal performance
}
Error Handling Patterns
// ✅ Proper error handling with intelligent storage
async saveWithErrorHandling(key: string, data: any) {
try {
const success = await this.storage.put(key, data);
if (success) {
console.log('✅ Data saved successfully');
return true;
} else {
console.warn('⚠️ Storage returned false - data may not be persisted');
return false;
}
} catch (error) {
console.error('❌ Storage error:', error);
// Storage system provides automatic failover, so errors are rare
return false;
}
}
🎯 Storage Key Naming Conventions
Our intelligent storage system analyzes key patterns to optimize storage decisions. Follow these conventions:
Critical Data Keys
// These keys trigger intelligent redundancy and prioritization
await storage.put('user/profile', profile); // User account data
await storage.put('auth/session/token', token); // Authentication data
await storage.put('settings/app/config', config); // Application settings
await storage.put('billing/subscription', billing); // Business-critical data
Cache Data Keys
// These keys are optimized for speed and structured storage
await storage.put('cache/api/users', apiResponse); // API response cache
await storage.put('cache/search/results', results); // Search results
await storage.put('cache/images/thumbnails', thumbs); // Media cache
Temporary Data Keys
// These keys are optimized for performance, minimal redundancy
await storage.put('temp/upload/file-123', fileData); // Temporary files
await storage.put('temp/draft/doc-456', draftData); // Draft documents
await storage.put('temp/session/state', sessionData); // Session state
Feature Data Keys
// Use your feature name as namespace
await storage.put('dashboard/widgets/config', widgets);
await storage.put('editor/documents/draft', document);
await storage.put('chat/conversations/history', chat);
🛡️ Best Practices
1. Trust the Intelligence
// ✅ GOOD: Let the system decide automatically
await storage.put('important-data', data);
// ❌ AVOID: Manual management unless debugging
storage.switchAdapter('opfs');
await storage.put('important-data', data);
2. Use Appropriate Data Types
// ✅ GOOD: Structured data with proper typing
interface UserProfile {
id: string;
name: string;
email: string;
preferences: UserPreferences;
}
await storage.put('user/profile', profile as UserProfile);
// ❌ AVOID: Unstructured or overly complex data
await storage.put('user/data', new ComplexClassInstance()); // May not serialize properly
3. Leverage Reactive Patterns
// ✅ GOOD: Use signals for reactive UI updates
readonly userTheme = this.storage.asSignal<string>('user/theme', 'light').value;
// ❌ AVOID: Manual polling or imperative updates
setInterval(async () => {
const theme = await this.storage.get('user/theme');
this.currentTheme = theme;
}, 1000);
4. Handle Offline Scenarios
// ✅ The system handles offline scenarios automatically
await storage.put('user/data', userData);
// Data is cached locally and synced when connection resumes
// Check storage capabilities if needed
const capabilities = storage.getCapabilities();
if (!capabilities.indexedDB) {
console.log('Limited storage - using localStorage fallback');
}
🔄 Migration from Legacy Storage
From localStorage
// ❌ Old pattern
localStorage.setItem('userData', JSON.stringify(data));
const userData = JSON.parse(localStorage.getItem('userData') || '{}');
// ✅ New pattern
await storage.put('user/data', data);
const userData = await storage.get('user/data');
From IndexedDB/Dexie
// ❌ Old pattern
await db.users.put({ id: userId, data: userData });
const userData = await db.users.get(userId);
// ✅ New pattern
await storage.put(`users/${userId}`, userData);
const userData = await storage.get(`users/${userId}`);
From Custom Storage Services
// ❌ Old pattern
@Injectable()
export class CustomStorageService {
async saveToIndexedDB(key: string, data: any) { /* manual IndexedDB */ }
async saveToLocalStorage(key: string, data: any) { /* manual localStorage */ }
async saveToCloud(key: string, data: any) { /* manual cloud */ }
}
// ✅ New pattern
@Injectable()
export class FeatureService {
private storage = inject(StorageService);
async saveData(key: string, data: any) {
// System handles IndexedDB, localStorage, cloud automatically
return await this.storage.put(key, data);
}
}
📋 Simple Implementation Checklist
✅ For New Features (RECOMMENDED - HTTP + Mock API)
// 1. Configure mock API with intelligent storage
import { mockHttpInterceptor, MockInterceptorUtilsService } from '@mock-api';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([mockHttpInterceptor])),
{
provide: APP_INITIALIZER,
multi: true,
deps: [MockInterceptorUtilsService],
useFactory: (utils: MockInterceptorUtilsService) => () => {
utils.configureMockInterceptor({
persistence: {
storage: 'intelligent', // Uses our storage system!
namespace: 'api-cache'
}
}, mockDefinitions);
}
}
]
};
// 2. Write services using HttpClient
@Injectable()
export class FeatureService {
private http = inject(HttpClient);
getData() { return this.http.get('/api/data'); }
saveData(data: any) { return this.http.post('/api/data', data); }
}
✅ For Legacy Features (Direct Storage)
// 1. Provider configuration with real factory
import { GgStorage as CoreGgStorage } from 'libs/storage-core/src/lib/gg-storage';
import type { GgStorage, GgStorageConfig } from 'libs/storage-angular/src/lib/tokens';
export function provideYourFeature() {
const storageConfig = {
namespace: 'your-feature',
strategy: 'durable' as const,
adapterPreference: 'dexie' as const,
performance: {
adaptiveStrategy: true,
batchProcessing: true,
microCacheBudget: 10 * 1024
}
};
// Type-safe wrapper that bridges storage-core and storage-angular
const createStorageWrapper = (config: GgStorageConfig): GgStorage => {
const coreStorage = new CoreGgStorage(config);
return {
get: (key: string) => coreStorage.get(key),
put: (key: string, value: any) => coreStorage.put(key, value),
remove: (key: string) => coreStorage.remove(key),
has: (key: string) => coreStorage.has(key),
list: (options?: any) => coreStorage.list(options),
clear: (prefix?: string) => coreStorage.clear(prefix),
getConfig: () => config,
onEvent: (listener: (event: any) => void) => coreStorage.onEvent(listener),
getAdapterKind: () => coreStorage.getAdapterKind?.() || 'localStorage',
switchAdapter: (kind: 'opfs' | 'dexie' | 'localStorage') => coreStorage.switchAdapter(kind)
} as GgStorage;
};
return makeEnvironmentProviders([
...provideGgStorage(storageConfig, createStorageWrapper),
YourFeatureService
]);
}
// 2. Simple service implementation
@Injectable()
export class YourFeatureService {
private readonly storage = inject(GG_STORAGE);
constructor() {
console.log('📊 Storage initialized:', {
adapter: this.storage.adapterKind,
strategy: this.storage.getStrategy?.()
});
}
// Use semantic keys for intelligence
async save(id: string, data: any): Promise<boolean> {
return await this.storage.put(`data/${id}`, data);
}
async load(id: string): Promise<any> {
return await this.storage.get(`data/${id}`);
}
async listAll(): Promise<any[]> {
const keys = await this.storage.list('data/');
const items = [];
for (const key of keys) {
const item = await this.storage.get(key);
if (item) items.push(item);
}
return items;
}
}
🎯 Key Principles
✅ DO
- Always provide real factory: Use the type-safe wrapper pattern shown above
- Use semantic keys:
themes/data/theme-123,users/profiles/user-456 - Trust the intelligence: Let the system choose adapters and strategies
- Keep it simple: Avoid manual forcing, nuclear options, or complex recovery
- Configure once: Set up proper config in providers, then trust it
❌ DON'T
- Never omit factory:
...provideGgStorage(config)gives you mock storage - Don't force adapters:
storage.switchAdapter()fights the intelligence - Don't use nuclear options: Complex forcing indicates design issues
- Don't complicate keys: Use semantic, hierarchical keys
- Don't bypass the system: Direct localStorage/IndexedDB defeats the purpose
🚀 Migration Pattern
For Existing Broken Services
- Fix the provider: Add real storage factory using type-safe wrapper
- Simplify the constructor: Remove forcing/nuclear code
- Use semantic keys: Replace complex key patterns
- Remove recovery code: The intelligent system handles persistence
- Test: Verify data actually persists after refresh
📚 Next Steps
- Smart Mock API Pattern - RECOMMENDED approach for all features
- Common Mistakes - Learn from real-world implementation issues
- Theme System Integration - See how we integrated themes
- Performance Guide - Optimize your storage usage
- Cloud Sync Guide - Add cloud synchronization
🎯 Summary: Choose Your Approach
🚀 RECOMMENDED: HTTP + Mock API
- Perfect for: All new features, API-ready development
- Benefits: Solid foundation, realistic testing, zero refactoring
- Performance: One-time HTTP cache, then storage speed
- Setup: Configure mock API with intelligent storage persistence
🔧 Alternative: Direct Storage
- Perfect for: Legacy features, utilities, internal tools
- Benefits: Direct access, minimal setup, maximum performance
- Setup: Provider configuration with real storage factory
Remember: Our intelligent storage system is designed to handle complexity automatically. Whether you choose HTTP + Mock API or direct storage, trust the intelligence, use semantic keys, and leverage reactive patterns for the best results!