Docs/cachingv1.0.0-rc.1

Cache Providers

A cache provider is any object that satisfies the ICacheProvider interface. UDIOT ships one built-in provider (MemoryCacheProvider) and lets you plug in custom backends (Redis, Memcached, DynamoDB, etc.) by implementing the same interface.

Key Concepts

  • Named registration. Providers are stored in CacheManager under a string key that matches a CacheStrategy value ('memory', 'redis', 'cdn', 'tiered').
  • Tag indexing. Each provider is responsible for tracking which keys carry which tags so that invalidateByTag() can remove them efficiently.
  • Bulk operations. getMany and setMany allow atomic batch reads and writes where the backend supports them.

API Reference

ICacheProvider

interface ICacheProvider {
  readonly name: string;

  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
  delete(key: string): Promise<boolean>;
  has(key: string): Promise<boolean>;
  clear(pattern?: string): Promise<void>;
  getMany<T>(keys: string[]): Promise<Map<string, T | null>>;
  setMany<T>(entries: Map<string, T>, options?: CacheSetOptions): Promise<void>;
  getStats(): Promise<CacheStats>;
  getKeysByTag(tag: string): Promise<string[]>;
  invalidateByTag(tag: string): Promise<number>;
}

CacheSetOptions:

Field Type Description
ttl number Time-to-live in seconds. Omit for no expiry.
tags string[] Tags for grouped invalidation
priority CachePriority 'low' | 'normal' | 'high' | 'critical' — hint for eviction order
compress boolean Reserved; hints to compress before storing

MemoryCacheProvider

An in-process LRU cache. Entries expire at read time; a background cleanup timer also runs periodically.

new MemoryCacheProvider(options?: MemoryCacheOptions)

MemoryCacheOptions:

Field Type Default Description
maxSize number 10000 Maximum number of entries
cleanupInterval number 60000 Expired-entry sweep interval in ms. Pass 0 to disable.

Additional methods specific to MemoryCacheProvider:

Method Description
resetStats() Zeroes hit/miss counters
destroy() Clears the cleanup timer; call before process exit

Pattern-based clearing via clear(pattern) translates the * wildcard to a regular expression and removes all matching keys.

Basic Usage

import { MemoryCacheProvider, CacheManager, setGlobalCacheManager } from '@tagna/udiot';

const memory = new MemoryCacheProvider({ maxSize: 5000, cleanupInterval: 30000 });

const cache = new CacheManager({
  providers: new Map([['memory', memory]]),
});
setGlobalCacheManager(cache);

Common Patterns

Registering a provider after construction

import { getGlobalCacheManager } from '@tagna/udiot';
import type { ICacheProvider, CacheSetOptions, CacheStats } from '@tagna/udiot';

// Assuming RedisProvider is implemented below
const redis = new RedisProvider({ host: 'localhost', port: 6379 });
getGlobalCacheManager().registerProvider('redis', redis);

Custom provider skeleton (Redis example)

import type { ICacheProvider, CacheSetOptions, CacheStats } from '@tagna/udiot';
import { createClient } from 'redis';

export class RedisProvider implements ICacheProvider {
  readonly name = 'redis';
  private client = createClient();
  private tagIndex: Map<string, Set<string>> = new Map();
  private hits = 0;
  private misses = 0;

  async get<T>(key: string): Promise<T | null> {
    const raw = await this.client.get(key);
    if (raw === null) { this.misses++; return null; }
    this.hits++;
    return JSON.parse(raw) as T;
  }

  async set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void> {
    const serialized = JSON.stringify(value);
    if (options?.ttl) {
      await this.client.setEx(key, options.ttl, serialized);
    } else {
      await this.client.set(key, serialized);
    }
    for (const tag of options?.tags ?? []) {
      if (!this.tagIndex.has(tag)) this.tagIndex.set(tag, new Set());
      this.tagIndex.get(tag)!.add(key);
    }
  }

  async delete(key: string): Promise<boolean> {
    const n = await this.client.del(key);
    return n > 0;
  }

  async has(key: string): Promise<boolean> {
    return (await this.client.exists(key)) > 0;
  }

  async clear(pattern?: string): Promise<void> {
    const keys = pattern
      ? await this.client.keys(pattern.replace(/\*/g, '*'))
      : await this.client.keys('*');
    if (keys.length) await this.client.del(keys);
  }

  async getMany<T>(keys: string[]): Promise<Map<string, T | null>> {
    const values = await this.client.mGet(keys);
    return new Map(keys.map((k, i) => [
      k,
      values[i] !== null ? (JSON.parse(values[i]!) as T) : null,
    ]));
  }

  async setMany<T>(entries: Map<string, T>, options?: CacheSetOptions): Promise<void> {
    for (const [key, value] of entries) await this.set(key, value, options);
  }

  async getStats(): Promise<CacheStats> {
    const info = await this.client.info('keyspace');
    return {
      hits: this.hits,
      misses: this.misses,
      hitRate: this.hits + this.misses > 0
        ? this.hits / (this.hits + this.misses) : 0,
      size: 0,   // byte size requires MEMORY USAGE per key
      keys: (await this.client.dbSize()),
    };
  }

  async getKeysByTag(tag: string): Promise<string[]> {
    return Array.from(this.tagIndex.get(tag) ?? []);
  }

  async invalidateByTag(tag: string): Promise<number> {
    const keys = await this.getKeysByTag(tag);
    if (keys.length) await this.client.del(keys);
    this.tagIndex.delete(tag);
    return keys.length;
  }
}

Integration Notes

  • The MemoryCacheProvider cleanup timer is unref'd so it does not prevent Node.js from exiting. Call destroy() explicitly if you need deterministic teardown.
  • All provider methods are async even if the underlying operation is synchronous; this keeps the interface consistent across backends with varying I/O models.
  • The priority and compress fields in CacheSetOptions are hints. MemoryCacheProvider reads priority when choosing which entry to evict under LRU but ignores compress.

Pitfalls

  • Do not share a single tagIndex Map between multiple application instances pointing at the same Redis cluster. Each instance maintains its own in-memory tag map; for multi-instance tag invalidation, store tags in Redis itself (e.g., as Redis Sets).
  • Setting cleanupInterval: 0 on MemoryCacheProvider disables the background sweep entirely. Expired entries are still evicted lazily on get, but the cache will not proactively free memory between reads.
From the canonical UDIOT public documentation.Try it in the playground ↗