Zum Inhalt springen
Deutsch

Response-Cache-Middleware

cached umhüllt einen Handler mit Response-Caching, verschlüsselt über eine selbst gelieferte key(req)-Funktion. Ein Cache-Treffer liefert die gespeicherte Response, ohne den darunterliegenden Handler aufzurufen.

import { cached, path, get } from 'actor-ts/http';
import { InMemoryCache } from 'actor-ts';
const productCache = cached({
cache: new InMemoryCache(),
ttlMs: 30_000,
key: (req) => `products:${pathParam(req, 'id')}`,
});
const routes = path('api',
path('products',
get(productCache(async (req) => /* expensive lookup */)),
),
);

30 Sekunden Cache pro Produkt-ID. Der erste Request trifft den Handler; nachfolgende GETs in den nächsten 30 s liefern die gecachte Response.

type ResponseCacheOptions = {
cache: Cache;
ttlMs: number;
key: (req: HttpRequest) => string | Promise<string>;
keyPrefix?: string; // default 'rsp:'
cacheStatuses?: ReadonlyArray<number>; // default [200]
};
FeldZweck
cacheDas Cache-Backend. InMemoryCache für Single-Node; Redis für geteilt.
ttlMsWie lange jede gecachte Response gültig bleibt.
key(req)Leitet den Cache-Key aus dem Request ab — enthält typischerweise URL-Params, Tenant-ID, Accept-Header usw. Pflicht.
keyPrefixCache-Key-Namespace. Default 'rsp:', damit sich mehrere Response-Caches im selben Redis nicht überschneiden.
cacheStatusesWelche Status-Codes cachebar sind. Default [200] — nur 2xx-Responses werden gecacht.

Die key(req)-Funktion ist der gesamte Cache-Key-Vertrag — was du dort aufnimmst, bestimmt, was separat gecacht wird. Gängige Muster:

// Per-URL-path key:
key: (req) => req.path,
// Per-tenant:
key: (req) => `${req.headers['x-tenant-id'] ?? 'public'}:${req.path}`,
// Per-locale + content type:
key: (req) => `${req.headers['accept-language']}:${req.headers['accept']}:${req.path}`,

Baue den Key so, dass er über äquivalente Requests deterministisch, aber über getrennt zu cachende Responses eindeutig ist.

GET /api/products → cache (stable, read-heavy)
POST /api/orders → don't wrap with cached() (mutation)
GET /api/users/me → cache only with per-user key
GET /api/feed?cursor=X → cache (one entry per cursor)

Der Wrapper ist eine Handler-Ebenen-Entscheidung — du wählst, welche Handler du umhüllst. Mutationen sollten typischerweise idempotent verwenden.

cached invalidiert nicht automatisch bei Writes — die Veraltung ist durch ttlMs begrenzt.

Für stärkere Konsistenz zwei Muster:

cached({ cache, ttlMs: 1_000, key }); // 1 second

Sub-Sekunden-TTLs begrenzen die Veraltung. Gut für „echtzeit-nahe” APIs.

// After a write that affects products:42, invalidate the cache:
await cache.delete('rsp:products:42'); // 'rsp:' is the default keyPrefix

Der Cache ist einfach ein Cache — du kannst den mit Prefix versehenen Key direkt deleten, wenn ein Write ihn invalidieren soll.

import { RedisCache, RedisCacheOptions } from 'actor-ts';
const redisCacheOptions = RedisCacheOptions.create().withUrl('redis://...');
const productCache = cached({
cache: new RedisCache(redisCacheOptions),
ttlMs: 30_000,
key: (req) => `products:${pathParam(req, 'id')}`,
});

Mit einem Redis-gestützten Cache teilen sich alle Pods denselben Cache — nützlich für Multi-Pod-Deployments, in denen du konsistentes Caching willst.

Für Per-Pod-Caches (jeder Pod hat seinen eigenen InMemoryCache) können Pods kurzzeitig unterschiedliche gecachte Responses liefern, bis sich die TTLs angleichen. Oft akzeptabel; hängt von den Konsistenzanforderungen ab.

cached({
cache, ttlMs: 30_000, key,
cacheStatuses: [200, 404], // cache both hits AND "not found"s
});

Cache 404s, wenn du viele Lookups für nicht existierende IDs hast — spart wiederholte DB-Zugriffe. Default ist nur [200], damit ein transientes 500 nicht 30 Sekunden lang hängen bleibt.