Skip to content

feat: Introduce cache namespaces #31

@helloscoopa

Description

@helloscoopa

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 namespace

Fluent 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

  1. Functionality: Complete namespace isolation with independent configurations
  2. Compatibility: Zero breaking changes for existing code
  3. Performance: <5% overhead for namespace operations
  4. Usability: Intuitive API with clear separation of concerns
  5. Storage: Efficient namespace-aware persistence
  6. Testing: 100% test coverage for namespace features

Risk Mitigation

  1. Memory Usage: Monitor memory consumption with multiple namespaces
  2. Storage Complexity: Ensure storage adapters handle namespaces efficiently
  3. API Confusion: Clear documentation distinguishing namespace vs global operations
  4. Migration Complexity: Smooth migration path from global to namespaced usage
  5. Performance Degradation: Optimize namespace lookup and switching

Future Enhancements

  1. Namespace Templates: Predefined namespace configurations for common use cases
  2. Dynamic Namespace Creation: Auto-create namespaces based on key patterns
  3. Namespace Policies: Advanced rules for namespace management and lifecycle
  4. Import/Export: Bulk operations for moving data between namespaces
  5. Namespace Analytics: Advanced monitoring and reporting for namespace usage

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions