Docs/cachingv1.0.0-rc.1

Cache Invalidation

UDIOT supports three invalidation strategies — exact key, glob pattern, and tag-based — through CacheInvalidator, TagRegistry, and PatternMatcher. Event-driven invalidation ties these strategies to domain events via CacheManager.setupEventInvalidation().

Key Concepts

  • TagRegistry maintains two bidirectional maps: tag → Set<key> and key → Set<tag>. Removing a key cleans up all tag mappings; removing a tag returns all associated keys.
  • PatternMatcher converts * wildcard patterns to regular expressions and filters an array of keys or delegates the sweep to a provider's clear(pattern).
  • CacheInvalidator orchestrates all three strategies and returns an InvalidationResult that reports which keys were removed and how long it took.
  • Event-driven invalidation is configured on CacheManager using setupEventInvalidation and triggered by calling handleEvent. This decouples business logic from cache management.

API Reference

TagRegistry

class TagRegistry {
  register(key: string, tags: string[]): void
  getKeysByTag(tag: string): string[]
  getTagsForKey(key: string): string[]
  removeKey(key: string): void
  removeByTag(tag: string): string[]   // returns removed keys
  clear(): void
  getAllTags(): string[]
  getStats(): { totalKeys: number; totalTags: number; avgTagsPerKey: number }
}

PatternMatcher

class PatternMatcher {
  static matches(key: string, pattern: string): boolean
  static filter(keys: string[], pattern: string): string[]
  static create(parts: (string | '*')[], separator?: string): string
  static extractParts(key: string, separator?: string): string[]
  static hasWildcard(pattern: string): boolean
  static toRegex(pattern: string): RegExp
}

Pattern syntax: * matches any sequence of characters. Patterns are anchored at both ends (i.e., user:* matches user:123 but not prefix:user:123).

CacheInvalidator

class CacheInvalidator {
  registerProvider(provider: ICacheProvider): void
  registerTags(key: string, tags: string[]): void
  invalidate(options: InvalidateOptions): Promise<InvalidationResult>
  getTagRegistry(): TagRegistry
  clear(): void
}

InvalidationResult:

Field Type Description
count number Number of keys invalidated
keys string[] Keys that were removed
duration number Time taken in milliseconds

Event-driven invalidation on CacheManager

interface EventInvalidationConfig {
  event: string;
  keys?:    string[] | ((payload: any) => string[]);
  tags?:    string[];
  pattern?: string  | ((payload: any) => string);
}

cacheManager.setupEventInvalidation(config: EventInvalidationConfig): void
cacheManager.handleEvent(eventName: string, payload: any): Promise<void>

When handleEvent is called, the manager resolves dynamic keys and pattern by invoking the function form with payload, then invalidates accordingly.

Basic Usage

Tag-based invalidation

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

const cache = new CacheManager();
setGlobalCacheManager(cache);

// Store with tags
await cache.set('user:42', userData, { ttl: 300, tags: ['users', 'user:42'] });

// Invalidate all 'users' entries
await cache.invalidate({ tags: ['users'] });

Pattern-based invalidation

import { PatternMatcher } from '@tagna/udiot';

// Build a pattern for all keys of a given class/method
const pattern = PatternMatcher.create(['udiot', 'userservice', 'getuser', '*']);
// → 'udiot:userservice:getuser:*'

await cache.invalidate({ pattern });

Using CacheInvalidator directly

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

const provider = new MemoryCacheProvider();
const invalidator = new CacheInvalidator();
invalidator.registerProvider(provider);
invalidator.registerTags('product:7', ['products', 'featured']);

const result = await invalidator.invalidate({ tags: ['featured'] });
console.log(`Removed ${result.count} keys in ${result.duration.toFixed(2)} ms`);

Common Patterns

Event-driven invalidation when a domain event fires

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

const cache = new CacheManager();
setGlobalCacheManager(cache);

// Register once at startup
cache.setupEventInvalidation({
  event: 'user.updated',
  keys: (payload: { userId: string }) => [`user:${payload.userId}`],
  tags: ['users'],
});

cache.setupEventInvalidation({
  event: 'product.price_changed',
  pattern: (payload: { category: string }) =>
    `udiot:productservice:getbycategory:${payload.category}:*`,
});

// Fire from your domain event handler:
async function onUserUpdated(event: { userId: string }) {
  await cache.handleEvent('user.updated', event);
}

Inspecting the tag registry

import { CacheInvalidator } from '@tagna/udiot';

const invalidator = new CacheInvalidator();
// ... register providers and tags ...

const stats = invalidator.getTagRegistry().getStats();
console.log('Tags in use:', stats.totalTags);
console.log('Keys tracked:', stats.totalKeys);
console.log('Avg tags/key:', stats.avgTagsPerKey.toFixed(2));

Integration Notes

  • The CacheManager maintains its own internal tagToKeys map in addition to calling invalidateByTag on each provider. This dual tracking ensures that cross-provider invalidation works even when a provider's own tag index differs.
  • Pattern-based clearing via provider.clear(pattern) converts * to .* in a regular expression. The match is anchored at both ends, so user:* will not accidentally match prefix:user:123.
  • CacheInvalidator does not currently return the keys removed by pattern-based clearing because provider.clear(pattern) does not expose a list of affected keys. Only tag-based and key-exact invalidations populate InvalidationResult.keys.

Pitfalls

  • Registering the same event in multiple setupEventInvalidation calls is additive; all registered handlers fire. This is intentional but can lead to duplicate delete calls if keys are listed in both a keys array and matched by a tag.
  • TagRegistry.removeByTag removes the tag entry and all its associated key entries. If a key had multiple tags, those other tags also lose the key from their sets, which prevents stale references.
  • Pattern matching uses ^ and $ anchors. Do not include regex metacharacters other than * in patterns; special characters are escaped before conversion.
From the canonical UDIOT public documentation.Try it in the playground ↗