-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Overview
Introduce cache namespaces to isolate different parts of an application, allowing different configuration settings per namespace and providing logical separation of cached data.
Background Analysis
Current State
- RunCache operates as a singleton with global configuration
- All cache operations work on a single, shared cache store
- Configuration is applied globally across all cache entries
- No built-in way to isolate cache data by application domain or feature
Namespace Requirements
- Isolation: Different namespaces should have separate cache stores
- Configuration: Each namespace can have its own settings (TTL, eviction policy, storage, etc.)
- API Consistency: Maintain familiar API while adding namespace support
- Performance: Minimal overhead for namespace operations
- Storage: Persistent storage should respect namespace boundaries
Implementation Strategy
Phase 1: Namespace Architecture
1.1 Namespace Manager Design
Create a central manager to handle multiple cache instances:
// src/core/namespace-manager.ts
export class NamespaceManager {
private static instance: NamespaceManager;
private namespaces: Map<string, CacheStore> = new Map();
private defaultNamespace: string = 'default';
static getInstance(): NamespaceManager {
if (!NamespaceManager.instance) {
NamespaceManager.instance = new NamespaceManager();
}
return NamespaceManager.instance;
}
async getNamespace(name: string): Promise<CacheStore> {
if (!this.namespaces.has(name)) {
const store = await CacheStore.create();
this.namespaces.set(name, store);
}
return this.namespaces.get(name)!;
}
async createNamespace(name: string, config?: CacheConfig): Promise<CacheStore> {
const store = await CacheStore.create(config);
this.namespaces.set(name, store);
return store;
}
listNamespaces(): string[] {
return Array.from(this.namespaces.keys());
}
async deleteNamespace(name: string): Promise<boolean> {
if (name === this.defaultNamespace) {
throw new Error('Cannot delete default namespace');
}
const store = this.namespaces.get(name);
if (store) {
await store.shutdown();
return this.namespaces.delete(name);
}
return false;
}
}1.2 Namespace Interface Design
Define the public interface for namespace operations:
// src/types/namespace.ts
export interface NamespaceConfig extends CacheConfig {
name: string;
isolated?: boolean; // Whether to use separate storage
}
export interface NamespaceStats {
name: string;
entryCount: number;
memoryUsage: number;
hitRate: number;
lastAccessed: number;
config: CacheConfig;
}
export interface NamespaceAPI {
// Core cache operations
set(params: SetParams): Promise<boolean>;
get(key: string): Promise<string | string[] | undefined>;
delete(key: string): Promise<boolean>;
has(key: string): Promise<boolean>;
flush(): Promise<void>;
refetch(key: string): Promise<boolean>;
// Configuration
configure(config: CacheConfig): Promise<void>;
getConfig(): Promise<CacheConfig>;
// Events
onExpiry(callback: EventCallback): Promise<void>;
onKeyExpiry(key: string, callback: EventCallback): Promise<void>;
// ... other event methods
// Stats and management
getStats(): Promise<NamespaceStats>;
shutdown(): Promise<void>;
}1.3 Storage Adapter Namespace Support
Update storage adapters to support namespace isolation:
// src/types/storage-adapter.ts
export interface NamespacedStorageAdapter extends StorageAdapter {
/**
* Save data for a specific namespace
*/
saveNamespace(namespace: string, data: string): Promise<void>;
/**
* Load data for a specific namespace
*/
loadNamespace(namespace: string): Promise<string | null>;
/**
* Clear data for a specific namespace
*/
clearNamespace(namespace: string): Promise<void>;
/**
* List all available namespaces
*/
listNamespaces(): Promise<string[]>;
}Phase 2: Core Namespace Implementation
2.1 Namespace Class Implementation
Create a wrapper class that implements the namespace API:
// src/core/namespace.ts
export class Namespace implements NamespaceAPI {
private store: CacheStore;
private name: string;
constructor(name: string, store: CacheStore) {
this.name = name;
this.store = store;
}
// Delegate all operations to the underlying store
async set(params: SetParams): Promise<boolean> {
return this.store.set(params);
}
async get(key: string): Promise<string | string[] | undefined> {
return this.store.get(key);
}
// ... implement all other cache operations
async getStats(): Promise<NamespaceStats> {
// Collect statistics from the underlying store
return {
name: this.name,
entryCount: this.store.size(),
memoryUsage: this.store.getMemoryUsage(),
hitRate: this.store.getHitRate(),
lastAccessed: this.store.getLastAccessed(),
config: this.store.getConfig(),
};
}
}2.2 Update CacheStore for Namespace Support
Add namespace-aware methods to CacheStore:
// src/core/cache-store.ts
export class CacheStore {
private namespace?: string;
constructor(config: CacheConfig = {}, namespace?: string) {
this.namespace = namespace;
// ... existing constructor logic
// Update storage adapter to use namespace
if (this.storageAdapter && namespace) {
this.updateStorageForNamespace(namespace);
}
}
private updateStorageForNamespace(namespace: string): void {
if (this.storageAdapter && 'saveNamespace' in this.storageAdapter) {
// Wrap storage operations to include namespace
const adapter = this.storageAdapter as NamespacedStorageAdapter;
this.saveToStorage = () => adapter.saveNamespace(namespace, this.serializeCache());
this.loadFromStorage = () => adapter.loadNamespace(namespace);
} else {
// For legacy adapters, prefix the storage key
this.updateStorageKey(namespace);
}
}
private updateStorageKey(namespace: string): void {
// Update storage key to include namespace prefix
const originalKey = this.storageAdapter?.storageKey || 'run-cache-data';
this.storageAdapter.storageKey = `${originalKey}:${namespace}`;
}
getNamespace(): string | undefined {
return this.namespace;
}
// Add methods for statistics
size(): number {
return this.cache.size;
}
getMemoryUsage(): number {
// Calculate approximate memory usage
let usage = 0;
for (const [key, state] of this.cache.entries()) {
usage += key.length * 2; // UTF-16 characters
usage += state.value.length * 2;
usage += 200; // Approximate overhead per entry
}
return usage;
}
getHitRate(): number {
// Track and return hit rate
return this.hitRate;
}
getLastAccessed(): number {
return this.lastAccessTime;
}
}Phase 3: RunCache API Updates
3.1 Namespace-Aware RunCache API
Update the main RunCache class to support namespaces:
// src/run-cache.ts
export class RunCache {
private static namespaceManager: NamespaceManager;
private static currentNamespace: string = 'default';
// Initialize namespace manager
static {
RunCache.namespaceManager = NamespaceManager.getInstance();
}
// Namespace management methods
static async createNamespace(name: string, config?: CacheConfig): Promise<Namespace> {
const store = await RunCache.namespaceManager.createNamespace(name, config);
return new Namespace(name, store);
}
static async getNamespace(name: string): Promise<Namespace> {
const store = await RunCache.namespaceManager.getNamespace(name);
return new Namespace(name, store);
}
static async useNamespace(name: string): Promise<void> {
RunCache.currentNamespace = name;
// Ensure namespace exists
await RunCache.namespaceManager.getNamespace(name);
}
static getCurrentNamespace(): string {
return RunCache.currentNamespace;
}
static listNamespaces(): string[] {
return RunCache.namespaceManager.listNamespaces();
}
static async deleteNamespace(name: string): Promise<boolean> {
return RunCache.namespaceManager.deleteNamespace(name);
}
// Update existing methods to use current namespace
private static async getCurrentStore(): Promise<CacheStore> {
return RunCache.namespaceManager.getNamespace(RunCache.currentNamespace);
}
static async set(params: SetParams): Promise<boolean> {
const store = await RunCache.getCurrentStore();
return store.set(params);
}
static async get(key: string): Promise<string | string[] | undefined> {
const store = await RunCache.getCurrentStore();
return store.get(key);
}
// ... update all existing methods to use getCurrentStore()
// Namespace-specific operations
static async configureNamespace(namespace: string, config: CacheConfig): Promise<void> {
const store = await RunCache.namespaceManager.getNamespace(namespace);
await store.configure(config);
}
static async getNamespaceStats(namespace?: string): Promise<NamespaceStats> {
const targetNamespace = namespace || RunCache.currentNamespace;
const ns = await RunCache.getNamespace(targetNamespace);
return ns.getStats();
}
static async getAllNamespaceStats(): Promise<NamespaceStats[]> {
const namespaces = RunCache.listNamespaces();
return Promise.all(
namespaces.map(name => RunCache.getNamespaceStats(name))
);
}
// Enhanced shutdown for all namespaces
static async shutdown(): Promise<void> {
const namespaces = RunCache.listNamespaces();
await Promise.all(
namespaces.map(async (name) => {
const store = await RunCache.namespaceManager.getNamespace(name);
await store.shutdown();
})
);
RunCache.namespaceManager.clear();
}
}3.2 Fluent Namespace API
Provide a fluent interface for namespace operations:
// src/namespace-builder.ts
export class NamespaceBuilder {
private config: NamespaceConfig = { name: '' };
static create(name: string): NamespaceBuilder {
return new NamespaceBuilder().name(name);
}
name(name: string): NamespaceBuilder {
this.config.name = name;
return this;
}
maxEntries(count: number): NamespaceBuilder {
this.config.maxEntries = count;
return this;
}
evictionPolicy(policy: EvictionPolicy): NamespaceBuilder {
this.config.evictionPolicy = policy;
return this;
}
storage(adapter: StorageAdapter): NamespaceBuilder {
this.config.storageAdapter = adapter;
return this;
}
isolated(isolated: boolean = true): NamespaceBuilder {
this.config.isolated = isolated;
return this;
}
async build(): Promise<Namespace> {
return RunCache.createNamespace(this.config.name, this.config);
}
}
// Usage example:
const userNamespace = await NamespaceBuilder
.create('users')
.maxEntries(1000)
.evictionPolicy(EvictionPolicy.LRU)
.storage(new IndexedDBAdapter({ storageKey: 'users-cache' }))
.build();Phase 4: Storage Adapter Updates
4.1 Update Existing Storage Adapters
Modify storage adapters to support namespaces:
// src/storage/local-storage-adapter.ts
export class LocalStorageAdapter implements NamespacedStorageAdapter {
private baseStorageKey: string;
constructor(config?: Partial<StorageAdapterConfig>) {
this.baseStorageKey = config?.storageKey || 'run-cache-data';
}
// Legacy methods (maintain compatibility)
async save(data: string): Promise<void> {
return this.saveNamespace('default', data);
}
async load(): Promise<string | null> {
return this.loadNamespace('default');
}
async clear(): Promise<void> {
return this.clearNamespace('default');
}
// New namespace methods
async saveNamespace(namespace: string, data: string): Promise<void> {
const key = this.getNamespaceKey(namespace);
window.localStorage.setItem(key, data);
}
async loadNamespace(namespace: string): Promise<string | null> {
const key = this.getNamespaceKey(namespace);
return window.localStorage.getItem(key);
}
async clearNamespace(namespace: string): Promise<void> {
const key = this.getNamespaceKey(namespace);
window.localStorage.removeItem(key);
}
async listNamespaces(): Promise<string[]> {
const namespaces: string[] = [];
const prefix = `${this.baseStorageKey}:`;
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key?.startsWith(prefix)) {
const namespace = key.substring(prefix.length);
namespaces.push(namespace);
}
}
return namespaces;
}
private getNamespaceKey(namespace: string): string {
return `${this.baseStorageKey}:${namespace}`;
}
}4.2 IndexedDB Namespace Support
Update IndexedDB adapter for namespace support:
// src/storage/indexed-db-adapter.ts
export class IndexedDBAdapter implements NamespacedStorageAdapter {
private dbName: string = 'run-cache-db';
private baseStoreName: string = 'cache-store';
async saveNamespace(namespace: string, data: string): Promise<void> {
const db = await this.initDB();
const storeName = this.getNamespaceStore(namespace);
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put({ id: this.storageKey, data });
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error('Failed to save namespace data'));
});
}
async loadNamespace(namespace: string): Promise<string | null> {
const db = await this.initDB();
const storeName = this.getNamespaceStore(namespace);
return new Promise<string | null>((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(this.storageKey);
request.onsuccess = () => {
resolve(request.result?.data || null);
};
request.onerror = () => reject(new Error('Failed to load namespace data'));
});
}
async listNamespaces(): Promise<string[]> {
const db = await this.initDB();
const storeNames = Array.from(db.objectStoreNames);
const prefix = `${this.baseStoreName}-`;
return storeNames
.filter(name => name.startsWith(prefix))
.map(name => name.substring(prefix.length));
}
private getNamespaceStore(namespace: string): string {
return `${this.baseStoreName}-${namespace}`;
}
// Update initDB to create namespace stores on demand
private async initDB(): Promise<IDBDatabase> {
// Enhanced initialization logic to handle dynamic store creation
}
}Phase 5: Advanced Features
5.1 Cross-Namespace Operations
Support operations across multiple namespaces:
// src/core/cross-namespace-operations.ts
export class CrossNamespaceOperations {
static async searchAcrossNamespaces(
pattern: string,
namespaces?: string[]
): Promise<{ namespace: string; key: string; value: string }[]> {
const targetNamespaces = namespaces || RunCache.listNamespaces();
const results: { namespace: string; key: string; value: string }[] = [];
for (const namespace of targetNamespaces) {
const store = await RunCache.namespaceManager.getNamespace(namespace);
const values = await store.get(pattern);
if (Array.isArray(values)) {
// Handle multiple matches
values.forEach((value, index) => {
results.push({ namespace, key: `${pattern}-${index}`, value });
});
} else if (values !== undefined) {
results.push({ namespace, key: pattern, value: values });
}
}
return results;
}
static async copyBetweenNamespaces(
fromNamespace: string,
toNamespace: string,
keyPattern: string
): Promise<number> {
const fromStore = await RunCache.namespaceManager.getNamespace(fromNamespace);
const toStore = await RunCache.namespaceManager.getNamespace(toNamespace);
// Implementation for copying cache entries
let copiedCount = 0;
// ... copy logic
return copiedCount;
}
static async syncNamespaces(
source: string,
target: string,
options?: { overwrite?: boolean; filter?: (key: string) => boolean }
): Promise<void> {
// Implementation for syncing namespaces
}
}5.2 Namespace Events
Add namespace-specific event handling:
// src/types/namespace-events.ts
export interface NamespaceEventParam extends EventParam {
namespace: string;
}
export const NAMESPACE_EVENT = {
NAMESPACE_CREATED: 'namespace_created',
NAMESPACE_DELETED: 'namespace_deleted',
NAMESPACE_CONFIGURED: 'namespace_configured',
CROSS_NAMESPACE_OPERATION: 'cross_namespace_operation',
} as const;
// Usage in RunCache
static async onNamespaceCreated(callback: (event: NamespaceEventParam) => void): Promise<void> {
// Register namespace creation event
}
static async onNamespaceDeleted(callback: (event: NamespaceEventParam) => void): Promise<void> {
// Register namespace deletion event
}Phase 6: Testing and Documentation
6.1 Comprehensive Test Suite
Create extensive tests for namespace functionality:
// src/namespaces/namespaces.test.ts
describe('Cache Namespaces', () => {
describe('Namespace Creation and Management', () => {
it('should create new namespaces with unique stores');
it('should list all available namespaces');
it('should delete namespaces and clean up resources');
it('should prevent deletion of default namespace');
});
describe('Namespace Isolation', () => {
it('should isolate data between namespaces');
it('should allow same keys in different namespaces');
it('should maintain separate configurations per namespace');
});
describe('Storage Integration', () => {
it('should persist namespaced data correctly');
it('should restore namespaced data on reload');
it('should handle storage adapter namespace support');
});
describe('Cross-Namespace Operations', () => {
it('should search across multiple namespaces');
it('should copy data between namespaces');
it('should sync namespaces with options');
});
describe('API Compatibility', () => {
it('should maintain backward compatibility for existing code');
it('should default to default namespace for legacy operations');
});
});6.2 Performance and Integration Tests
Test namespace performance and integration:
// src/namespaces/performance.test.ts
describe('Namespace Performance', () => {
it('should have minimal overhead for namespace operations');
it('should scale well with many namespaces');
it('should efficiently manage memory across namespaces');
});
// src/namespaces/integration.test.ts
describe('Namespace Integration', () => {
describe('With Existing Features', () => {
it('should work with middleware');
it('should work with eviction policies');
it('should work with TTL and auto-refetch');
it('should work with tags and dependencies');
});
});Usage Examples
Basic Namespace Usage
import { RunCache, NamespaceBuilder } from 'run-cache';
// Create namespaces for different application areas
const userCache = await RunCache.createNamespace('users', {
maxEntries: 1000,
evictionPolicy: EvictionPolicy.LRU
});
const sessionCache = await RunCache.createNamespace('sessions', {
maxEntries: 500,
defaultTTL: 30 * 60 * 1000 // 30 minutes
});
// Use namespace-specific operations
await userCache.set({ key: 'user:123', value: 'John Doe' });
await sessionCache.set({ key: 'session:abc', value: 'active' });
// Switch default namespace context
await RunCache.useNamespace('users');
await RunCache.set({ key: 'user:456', value: 'Jane Smith' }); // Goes to users namespaceFluent Builder Pattern
const productCache = await NamespaceBuilder
.create('products')
.maxEntries(5000)
.evictionPolicy(EvictionPolicy.LFU)
.storage(new IndexedDBAdapter({ storageKey: 'products' }))
.isolated(true)
.build();
await productCache.set({
key: 'product:123',
value: JSON.stringify({ name: 'Widget', price: 29.99 }),
tags: ['electronics', 'widgets']
});Cross-Namespace Operations
// Search across multiple namespaces
const results = await CrossNamespaceOperations.searchAcrossNamespaces(
'user:*',
['users', 'profiles', 'sessions']
);
// Copy data between namespaces
await CrossNamespaceOperations.copyBetweenNamespaces(
'staging-users',
'production-users',
'user:verified:*'
);Namespace Statistics and Monitoring
// Get statistics for specific namespace
const userStats = await RunCache.getNamespaceStats('users');
console.log(`Users cache: ${userStats.entryCount} entries, ${userStats.memoryUsage} bytes`);
// Get statistics for all namespaces
const allStats = await RunCache.getAllNamespaceStats();
allStats.forEach(stats => {
console.log(`${stats.name}: hit rate ${stats.hitRate}%`);
});File Structure
src/
├── core/
│ ├── namespace-manager.ts # Central namespace management
│ ├── namespace.ts # Individual namespace implementation
│ ├── cache-store.ts # Updated with namespace support
│ └── cross-namespace-operations.ts # Cross-namespace utilities
├── types/
│ ├── namespace.ts # Namespace type definitions
│ └── namespace-events.ts # Namespace event types
├── namespaces/
│ ├── namespace-builder.ts # Fluent builder interface
│ ├── namespaces.test.ts # Core namespace tests
│ ├── integration.test.ts # Integration tests
│ └── performance.test.ts # Performance benchmarks
├── storage/
│ ├── local-storage-adapter.ts # Updated with namespace support
│ ├── indexed-db-adapter.ts # Updated with namespace support
│ └── filesystem-adapter.ts # Updated with namespace support
└── run-cache.ts # Updated main API
Timeline Estimation
Phase 1 (Week 1): Architecture Design
- Namespace manager and core interfaces
- Storage adapter interface updates
- Initial namespace class structure
Phase 2 (Week 2): Core Implementation
- Namespace manager implementation
- Update CacheStore for namespace support
- Basic namespace operations
Phase 3 (Week 3): API Integration
- Update RunCache API for namespaces
- Fluent builder interface
- Backward compatibility layer
Phase 4 (Week 4): Storage Updates
- Update all storage adapters for namespace support
- Migration and compatibility handling
- Persistent namespace management
Phase 5 (Week 5): Advanced Features
- Cross-namespace operations
- Namespace events and monitoring
- Performance optimizations
Phase 6 (Week 6): Testing & Documentation
- Comprehensive test suite
- Performance benchmarks
- Documentation and examples
Success Metrics
- Functionality: Complete namespace isolation with independent configurations
- Compatibility: Zero breaking changes for existing code
- Performance: <5% overhead for namespace operations
- Usability: Intuitive API with clear separation of concerns
- Storage: Efficient namespace-aware persistence
- Testing: 100% test coverage for namespace features
Risk Mitigation
- Memory Usage: Monitor memory consumption with multiple namespaces
- Storage Complexity: Ensure storage adapters handle namespaces efficiently
- API Confusion: Clear documentation distinguishing namespace vs global operations
- Migration Complexity: Smooth migration path from global to namespaced usage
- Performance Degradation: Optimize namespace lookup and switching
Future Enhancements
- Namespace Templates: Predefined namespace configurations for common use cases
- Dynamic Namespace Creation: Auto-create namespaces based on key patterns
- Namespace Policies: Advanced rules for namespace management and lifecycle
- Import/Export: Bulk operations for moving data between namespaces
- Namespace Analytics: Advanced monitoring and reporting for namespace usage
Metadata
Metadata
Assignees
Labels
Projects
Status