Intelligent Adapter Selection - How It Works
Complete guide to the storage-system's autonomous adapter selection mechanism
🧠 Intelligent Adapter Selection - How It Works
Core Philosophy: The storage-system is fully autonomous - you never manually specify which adapter to use. The system analyzes data characteristics and makes optimal decisions automatically.
⚡ TL;DR
// ✅ CORRECT: Let the system decide
await storage.put('user/profile', userData);
// System automatically analyzes:
// - Size: 15KB (medium)
// - Key pattern: 'user/*' (critical)
// - Decision: IndexedDB + localStorage redundancy
// ❌ WRONG: Never manually specify adapter
const eventData: StorageEventData = {
type: 'put',
key: 'universe_123',
adapter: 'opfs' // ❌ DON'T DO THIS - defeats intelligent system!
};
Rule: Never set the adapter field manually - it's populated automatically by the storage engine based on intelligent analysis.
🏗️ How Adapter Selection Works
Step 1: Data Analysis (Automatic)
When you call storage.put(), the system analyzes:
// File: gg-storage.ts, line 1055-1080
private makeDecision<T>(key: string, value: T, operation: 'put' | 'get' | 'remove'): PolicyDecision {
const request = {
key,
size: this.estimateSize(value), // 🔍 Calculate data size
operation,
strategy: this.strategy, // 'speed' | 'balanced' | 'durable'
capabilities: this.capabilities, // What adapters are available
flags: {
redundancy: this.config.performance?.redundancy,
monitoring: this.config.performance?.monitoring
}
};
// Use adaptive engine for enhanced decision making
if (this.config.performance?.adaptiveStrategy !== false) {
return this.adaptivePolicyEngine.makeAdaptiveDecision(request);
}
// Fallback to standard policy engine
return decide(request);
}
Step 2: Policy Engine Decision (Automatic)
The policy engine (decide() function in policy-engine.ts) analyzes:
// File: policy-engine.ts, line 184-268
export function decide(request: PolicyRequest): PolicyDecision {
const { key, size, strategy, capabilities } = request;
// 🔍 Classify key based on patterns
const keyType = classifyKey(key);
// Returns: 'critical' | 'user-preference' | 'cache' | 'temp' | 'data'
// 🔍 Analyze size
const isSmall = size < 8 * 1024; // < 8KB
const isMedium = size < 64 * 1024; // 8KB - 64KB
const isLarge = size >= 64 * 1024; // 64KB+
const isHuge = size >= 512 * 1024; // 512KB+
// 🧠 Make intelligent decisions
const useMicroCache = strategy === 'speed' && isSmall && (isCritical || isUserPref);
const useOPFS = capabilities.opfs && !isTemp;
// Dexie decision logic...
// Write mode logic...
// Redundancy logic...
return {
target: { opfs, dexie, localStorage },
redundancy: { localStorage, dexie },
writeMode: 'await-opfs' | 'queue-opfs' | 'dexie-only',
ttlMs,
priority,
// ... etc
};
}
Step 3: Key Classification (Automatic)
// File: policy-engine.ts, line 123-149
export function classifyKey(key: string): KeyType {
const baseKey = getBaseKey(key); // Extracts base from scoped keys
// 🔍 Critical system data - needs redundancy
if (baseKey.startsWith('auth/') ||
baseKey.startsWith('session/') ||
baseKey.includes('critical/')) {
return 'critical';
}
// 🔍 User preferences - important but not critical
if (baseKey.startsWith('user/') ||
baseKey.startsWith('profile/') ||
baseKey.includes('settings/')) {
return 'user-preference';
}
// 🔍 Cache data - can be regenerated
if (baseKey.startsWith('cache/') || baseKey.includes('cached/')) {
return 'cache';
}
// 🔍 Temporary data - short-lived
if (baseKey.startsWith('temp/') || baseKey.startsWith('tmp/')) {
return 'temp';
}
return 'data'; // Default
}
📊 Decision Matrix (Automatic)
The system makes these decisions automatically based on data characteristics:
By Data Size
| Size | Classification | Primary Adapter | Micro-Cache | Redundancy |
|---|---|---|---|---|
| < 8KB | Small | IndexedDB | ✅ Yes (if critical) | localStorage |
| 8KB - 64KB | Medium | IndexedDB | ❌ No | IndexedDB |
| 64KB - 512KB | Large | OPFS | ❌ No | IndexedDB index |
| > 512KB | Huge | OPFS | ❌ No | IndexedDB index |
By Key Pattern
| Key Pattern | Classification | Storage | Redundancy |
|---|---|---|---|
auth/* |
Critical | IndexedDB | ✅ localStorage + Dexie |
session/* |
Critical | IndexedDB | ✅ localStorage + Dexie |
user/* |
User Preference | IndexedDB | ✅ localStorage (if small) |
profile/* |
User Preference | IndexedDB | ✅ localStorage (if small) |
cache/* |
Cache | IndexedDB | ❌ None |
temp/* |
Temporary | localStorage | ❌ None |
universe_* |
Data | OPFS/IndexedDB | Depends on size |
By Strategy
| Strategy | Small Data | Medium Data | Large Data | Write Mode |
|---|---|---|---|---|
speed |
localStorage (micro) | IndexedDB | OPFS (queued) | Background writes |
balanced |
IndexedDB | IndexedDB | OPFS (immediate) | Await writes |
durable |
IndexedDB + localStorage | IndexedDB + localStorage | OPFS (immediate) | Await + verify |
🎯 Real-World Examples
Example 1: Universe Creation
// Creating a new universe
const universe = {
id: 'universe_123',
name: 'My Universe',
galaxyIds: [],
metadata: { /* ... */ }
};
// ✅ CORRECT: Let system decide
await storage.put('universe_universe_123', universe);
// 🧠 System analyzes:
// - Key: 'universe_universe_123' → type: 'data'
// - Size: ~2KB → 'small'
// - Strategy: 'balanced' (default)
// - Decision: IndexedDB (no redundancy needed for non-critical data)
Example 2: User Authentication
// Storing auth token
const authToken = {
token: 'jwt_token_here',
expiresAt: Date.now() + 3600000
};
// ✅ CORRECT: Let system decide
await storage.put('auth/session/token', authToken);
// 🧠 System analyzes:
// - Key: 'auth/session/token' → type: 'critical'
// - Size: ~200 bytes → 'small'
// - Strategy: 'balanced'
// - Decision: IndexedDB + localStorage redundancy (critical data!)
Example 3: Large Document
// Storing a large PDF document
const pdfData = new Uint8Array(2 * 1024 * 1024); // 2MB
// ✅ CORRECT: Let system decide
await storage.put('documents/large-file', pdfData);
// 🧠 System analyzes:
// - Key: 'documents/large-file' → type: 'data'
// - Size: 2MB → 'huge'
// - Strategy: 'balanced'
// - Decision: OPFS (optimal for large files) + IndexedDB index
Example 4: Temporary Cache
// Caching API response
const apiResponse = { /* API data */ };
// ✅ CORRECT: Let system decide
await storage.put('cache/api/users', apiResponse);
// 🧠 System analyzes:
// - Key: 'cache/api/users' → type: 'cache'
// - Size: ~5KB → 'small'
// - Strategy: 'balanced'
// - Decision: IndexedDB only (no redundancy for cache)
🚫 Common Mistakes
❌ Mistake 1: Manually Specifying Adapter
// ❌ WRONG: Defeats intelligent system
const eventData: StorageEventData = {
type: 'put',
key: 'universe_123',
adapter: 'opfs', // DON'T DO THIS!
timestamp: Date.now()
};
// ✅ CORRECT: Let system populate adapter field
const eventData: StorageEventData = {
type: 'put',
key: 'universe_123',
timestamp: Date.now()
// adapter is optional - storage-system fills it in automatically
};
❌ Mistake 2: Assuming Data Goes to Specific Adapter
// ❌ WRONG: Assuming small data goes to localStorage
await storage.put('user/settings', settings);
// Reality: Goes to IndexedDB + localStorage redundancy
// ✅ CORRECT: Trust the system
await storage.put('user/settings', settings);
// System decides based on size, pattern, strategy
❌ Mistake 3: Pre-Optimizing Storage Location
// ❌ WRONG: Trying to optimize manually
if (dataSize > 1024 * 1024) {
await opfsStorage.put(key, value); // Manually choosing OPFS
} else {
await indexedDBStorage.put(key, value);
}
// ✅ CORRECT: Let system optimize
await storage.put(key, value);
// System makes better decisions based on:
// - Current adapter load
// - Cache hit rates
// - Historical patterns
// - Available capacity
🤖 AI-Powered Adaptive Learning
The system gets smarter over time via the AdaptivePolicyEngine:
// File: intelligence/adaptive-policy-engine.ts
export class AdaptivePolicyEngine {
/**
* Make adaptive decision based on historical data + policy engine
*/
makeAdaptiveDecision(request: PolicyRequest): PolicyDecision {
// 1. Get base decision from policy engine
const baseDecision = decide(request);
// 2. Check historical performance for this key
const history = this.getKeyHistory(request.key);
// 3. Adjust decision based on patterns
if (history.accessCount > 100 && history.avgDuration > 10) {
// Frequently accessed but slow? Promote to faster storage!
baseDecision.target.localStorage = 'micro';
}
// 4. Learn from this operation
this.recordOperationResult(request.key, request.operation, {
duration: 0, // Will be filled after operation
success: true,
cacheHit: false,
adapter: this.determineAdapterFromDecision(baseDecision)
});
return baseDecision;
}
}
Result: The system learns which keys are "hot" and automatically promotes them to faster storage!
📈 Performance Intelligence
Automatic Optimization Examples
Scenario 1: User profile accessed frequently
// First access: 20ms (IndexedDB)
const profile = await storage.get('user/profile');
// After 10 accesses, system learns it's "hot"
// Next access: 0.3ms (promoted to RAM cache automatically)
const profileFast = await storage.get('user/profile');
Scenario 2: Storage quota filling up
// System detects: IndexedDB at 85% capacity
// Automatic migration triggered:
// - Large items migrate to OPFS
// - IndexedDB space freed
// - No user intervention needed
Scenario 3: Network conditions change
// Online: Sync to MongoDB in background
// Offline: Queue changes locally
// Online again: Auto-sync queued changes
// All automatic - no manual intervention!
🎯 Best Practices
✅ DO: Trust the System
// The system makes better decisions than manual code
await storage.put(key, value);
await storage.get(key);
await storage.remove(key);
✅ DO: Use Semantic Key Names
// Good key names help the classifier
await storage.put('auth/session/token', token); // → Critical
await storage.put('user/preferences/theme', theme); // → User preference
await storage.put('cache/api/response', data); // → Cache
await storage.put('temp/upload/progress', progress); // → Temporary
✅ DO: Configure Strategy, Not Adapters
// app.config.ts
provideIntelligentStorage({
namespace: 'my-app',
strategy: 'balanced', // 🎯 Configure behavior, not implementation
performance: {
adaptiveStrategy: true, // ✅ Enable AI learning
redundancy: true, // ✅ Enable smart redundancy
monitoring: true // ✅ Enable performance tracking
}
})
❌ DON'T: Manually Specify Adapters
// ❌ WRONG: Bypasses intelligence
const eventData: StorageEventData = {
adapter: 'opfs' // Defeats the entire intelligent system!
};
// ❌ WRONG: Manual adapter selection
if (dataSize > threshold) {
await opfsAdapter.write(key, value);
} else {
await indexedDBAdapter.write(key, value);
}
// ✅ CORRECT: Trust the policy engine
await storage.put(key, value);
🔬 Under the Hood
The Decision Pipeline
graph TD
A[storage.put(key, value)] --> B[estimateSize(value)]
B --> C[makeDecision()]
C --> D[classifyKey(key)]
C --> E[Check capabilities]
C --> F[Apply strategy]
D --> G{Key Type?}
G -->|critical| H[IndexedDB + localStorage]
G -->|user-preference| I[IndexedDB + localStorage if small]
G -->|cache| J[IndexedDB only]
G -->|temp| K[localStorage only]
G -->|data| L{Size?}
L -->|Small| M[IndexedDB]
L -->|Medium| N[IndexedDB]
L -->|Large| O[OPFS + IndexedDB index]
L -->|Huge| P[OPFS + IndexedDB index + CAS]
Example Decision Flow
// Input
await storage.put('universe_123456', {
id: 'universe_123456',
name: 'My Universe',
galaxyIds: ['galaxy1', 'galaxy2'],
metadata: { /* ... */ }
});
// Step 1: Estimate size
size = JSON.stringify(value).length * 2; // ~3KB
// Step 2: Classify key
classifyKey('universe_123456');
// → 'data' (doesn't match critical/user/cache/temp patterns)
// Step 3: Analyze size
// 3KB < 8KB → isSmall = true
// Step 4: Check strategy
// strategy = 'balanced' (default)
// Step 5: Make decision
return {
target: {
opfs: false, // Not large enough for OPFS
dexie: 'inline', // Store in IndexedDB directly
localStorage: 'none' // No micro-cache (not critical)
},
redundancy: {
localStorage: false, // Not critical
dexie: false
},
writeMode: 'dexie-only',
priority: 'normal'
};
// Result: Universe saved to IndexedDB
🎨 Storage Event Data
What StorageEventData Is For
The StorageEventData interface is for event metadata, not for controlling storage:
export interface StorageEventData {
type: StorageEventType; // 'put' | 'get' | 'delete' | 'clear'
key?: string; // Which key was affected
value?: unknown; // (Optional) The value
adapter?: StorageAdapterType; // ✅ FILLED BY SYSTEM (not by you!)
timestamp: number; // When the event occurred
error?: string; // Error message if failed
}
Purpose: Track what happened, not control what should happen.
How to Use Storage Events
// ✅ CORRECT: Listen to events, don't control them
storage.onEvent((event) => {
console.log(`Storage event: ${event.type} on ${event.key}`);
console.log(`Adapter used: ${event.adapter}`); // ✅ Read the adapter decision
// React to the event
if (event.type === 'put' && event.key === 'universe_123') {
// Refresh UI, etc.
}
});
// ❌ WRONG: Don't create events manually with adapter specified
const fakeEvent: StorageEventData = {
type: 'put',
adapter: 'opfs', // ❌ This overrides the intelligent decision!
timestamp: Date.now()
};
🔧 When Adapter Field is Populated
The adapter field in StorageEventData is populated automatically by the storage engine when it emits events:
// File: gg-storage.ts, line 469-487
if (primarySuccess) {
this.cache.set(key, value);
// 🔥 Emit event with adapter information
this.emit({ type: 'put', key });
// Note: this.emit() is internal - it knows which adapter was used
// System can extend this to include adapter info in the future
// For now, consumers don't need to know the adapter -
// they just react to the event
}
Current Implementation
Currently, emit() emits a simple StorageEvent:
type StorageEvent = {
type: 'put' | 'delete' | 'clear' | 'remove';
key?: string
};
Then consumers convert it to StorageEventData by adding metadata:
// ✅ CORRECT: Add only metadata you know (timestamp)
this.storage.onEvent((event) => {
const eventData: StorageEventData = {
...event,
type: event.type === 'remove' ? 'delete' : event.type,
timestamp: Date.now()
// adapter is optional - system fills it in during emission if needed
};
void this.handleStorageEvent(eventData);
});
💡 Key Insights
1. Adapter is an Output, Not an Input
// Think of 'adapter' as a READ-ONLY field
interface StorageEventData {
adapter?: StorageAdapterType; // "What adapter was used" not "Use this adapter"
}
2. Intelligence Happens During write/read, Not During Events
// Intelligence applied HERE ↓
await storage.put('universe_123', universe);
// Decision made: "IndexedDB is optimal for this data"
// Event emitted HERE ↓
// Just notifies: "Data was stored, adapter was IndexedDB"
storage.onEvent((e) => {
// React to the fact that data changed
// Don't try to control where it goes
});
3. System Learns and Adapts
// First 10 accesses to 'user/settings'
// Average: 15ms (IndexedDB)
// System learns: "This is hot data"
// Promotion: Moves to RAM cache
// Next 100 accesses to 'user/settings'
// Average: 0.3ms (RAM cache)
// All automatic - no code changes needed!
🎓 Summary
Core Principles
- Never manually specify adapters - The system is smarter than manual code
- Use semantic key patterns - Help the classifier understand your data
- Configure strategy, not implementation - Set 'speed'/'balanced'/'durable', not 'use OPFS'
- Trust the intelligence - The system learns and optimizes over time
Why This Matters
- Better Performance: AI learns your patterns and optimizes automatically
- Better Reliability: Critical data gets redundancy without manual code
- Better Scalability: Auto-migration handles capacity automatically
- Better DX: Write less code, get better results
The Golden Rule
// 🎯 THE GOLDEN RULE 🎯
// Your job: Tell the system WHAT to store
// System's job: Decide HOW to store it
await storage.put(key, value); // ✅ That's it!
📚 Related Documentation
- Policy Engine Deep Dive - How decisions are made
- Adaptive Learning - How the system learns
- Performance Architecture - Performance optimization
- Getting Started - Quick start guide
Remember: The storage-system is a Ferrari - you don't need to tell it how to shift gears, just where you want to go! 🏎️
Created: November 19, 2025 - 12:00 PM
Session ID: 11.25.1.2
Status: ✅ Complete