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
CacheManagerunder a string key that matches aCacheStrategyvalue ('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.
getManyandsetManyallow 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
MemoryCacheProvidercleanup timer is unref'd so it does not prevent Node.js from exiting. Calldestroy()explicitly if you need deterministic teardown. - All provider methods are
asynceven if the underlying operation is synchronous; this keeps the interface consistent across backends with varying I/O models. - The
priorityandcompressfields inCacheSetOptionsare hints.MemoryCacheProviderreadsprioritywhen choosing which entry to evict under LRU but ignorescompress.
Pitfalls
- Do not share a single
tagIndexMap 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: 0onMemoryCacheProviderdisables the background sweep entirely. Expired entries are still evicted lazily onget, but the cache will not proactively free memory between reads.
From the canonical UDIOT public documentation.Try it in the playground ↗