Перейти к содержимому
Русский

Response cache middleware

Это содержимое пока не доступно на вашем языке.

cached wraps a handler with response caching keyed by a user-supplied key(req) function. A cache hit serves the stored response without invoking the underlying handler.

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 */)),
),
);

Cache for 30 seconds per product ID. First request hits the handler; subsequent GETs in the next 30 s return the cached response.

interface ResponseCacheOptions {
cache: Cache;
ttlMs: number;
key: (req: HttpRequest) => string | Promise<string>;
keyPrefix?: string; // default 'rsp:'
cacheStatuses?: ReadonlyArray<number>; // default [200]
}
FieldPurpose
cacheThe Cache backend. InMemoryCache for single-node; Redis for shared.
ttlMsHow long each cached response stays valid.
key(req)Derive the cache key from the request — typically includes URL params, tenant ID, accept header etc. Required.
keyPrefixCache-key namespace. Default 'rsp:' so multiple response-caches in the same Redis don’t collide.
cacheStatusesWhich status codes are cacheable. Default [200] — only 2xx responses get cached.

The key(req) function is the entire cache-key contract — what you include there determines what’s cached separately. Common patterns:

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

Build the key to be deterministic across equivalent requests but unique across responses you want cached separately.

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)

The wrapper is a handler-level decision — you choose which handlers to wrap. Mutations should typically use idempotent instead.

cached doesn’t auto-invalidate on writes — staleness is bounded by ttlMs.

For stronger consistency, two patterns:

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

Sub-second TTLs limit staleness. Good for “real-time-ish” APIs.

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

The cache is just a Cache — you can delete the prefixed key directly when a write should invalidate it.

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

With a Redis-backed cache, all pods share the same cache — useful for multi-pod deployments where you want consistent caching.

For per-pod caches (each pod has its own InMemoryCache), pods may serve different cached responses briefly until TTLs align. Often acceptable; depends on consistency requirements.

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

Cache 404s when you’ve got many lookups for IDs that don’t exist — saves repeat DB hits. Defaults to [200] only so a transient 500 never sticks around for 30 seconds.