Caching & Compression
Three pieces, all modelled on ASP.NET Core: useResponseCompression() streams gzip/brotli, the output cache stores complete 200 responses per route with [OutputCache]-style policies, and master.cache is an IDistributedCache backed by memory (LRU) or Redis.
| ASP.NET Core | MasterController |
|---|---|
UseResponseCompression() | master.useResponseCompression({ threshold, level, brotli, brotliQuality, mimeTypes }) |
[OutputCache(Duration, VaryByQueryKeys, Tags)] | route { outputCache: { duration, varyByQuery, varyByHeader, varyByUser, tags, cacheAuthenticated } }, controller static outputCache, router.resources(name, { outputCache }) |
AddOutputCache(o => o.AddPolicy("Short", …)) | master.outputCache.addPolicy('Short', { duration: 10 }) → { outputCache: 'Short' } |
IOutputCacheStore.EvictByTagAsync | await master.outputCache.evictByTag('posts') |
IDistributedCache / IMemoryCache | master.cache.get / set / remove / getOrCreate / evictByTag / clear |
| Redis distributed cache | master.cache.use(new RedisCacheStore(ioredisClient, { prefix })) |
At a glance#
const server = master.setupServer('http');
master.useResponseCompression(); // before start(); clients with Accept-Encoding get gzip/br
master.outputCache.addPolicy('Short', { duration: 10, tags: ['catalog'] });
// routes
router.route('/products', 'products#index', 'get', { outputCache: { duration: 60, varyByQuery: ['page', 'sort'], tags: ['products'] } });
router.route('/products/:id(int)', 'products#show', 'get', { outputCache: 'Short' });
// controller-level
export default class ReportsController { static outputCache = { index: { duration: 300 } }; }
// invalidate after writes
await master.outputCache.evictByTag('products');
// app cache
const cats = await master.cache.getOrCreate('categories', () => db.Category.toList(), { ttlMs: 60_000, tags: ['catalog'] });Response compression#
master.useResponseCompression({
threshold: 1024, // skip bodies below this when Content-Length is known
level: 6, // gzip level
brotli: true, // prefer br when the client accepts it
brotliQuality: 4,
mimeTypes: undefined, // default: text/*, JSON, JS, XML, SVG
});Compression skips: no acceptable encoding, an existing Content-Encoding, 204/304/HEAD, Cache-Control: no-transform, non-compressible content types, and known-length bodies below threshold. It sets Content-Encoding and Vary: Accept-Encoding and streams — no buffering. Compression wraps output caching, so cache hits are compressed per client.
Output caching#
// route option — [OutputCache(Duration = 60, VaryByQueryKeys = …, Tags = …)]
router.route('/products', 'products#index', 'get', {
outputCache: {
duration: 60, // seconds
varyByQuery: ['page', 'sort'], // or '*' (default)
varyByHeader: ['accept-language'],
varyByUser: false, // true: one entry per authenticated user (implies cacheAuthenticated)
tags: ['products'],
cacheAuthenticated: false, // authenticated requests bypass the cache by default
maxBodyBytes: 1024 * 1024,
},
});
// named policies — AddOutputCache(o => o.AddPolicy("Short", …))
master.outputCache.addPolicy('Short', { duration: 10 });
router.route('/products/:id(int)', 'products#show', 'get', { outputCache: 'Short' });
// controllers and resources
export default class ReportsController {
static outputCache = { index: { duration: 300 }, '*': 'Short' };
}
router.resources('posts', { outputCache: { duration: 30 } }); // applies to index and show
// eviction — IOutputCacheStore.EvictByTagAsync
await master.outputCache.evictByTag('products');
await master.outputCache.clear();Semantics#
- Only
GET/HEAD, only200responses, bodies up tomaxBodyBytes(1 MB). - Not when the request is authenticated (
Authorizationheader orthis.user) unlesscacheAuthenticated: trueorvaryByUser. - Not when the response sets cookies or
Cache-Control: no-store|private. - Runs after authorization and model binding, so a protected route never leaks through the cache.
- Responses carry
X-Output-Cache: HIT|MISSandAge.
Distributed cache#
master.cache stores values with absolute (ttlMs) or sliding (slidingMs) expiry and tags. getOrCreate is single-flight: concurrent misses share one factory call. The default store is an in-memory LRU; swap in RedisCacheStore for multiple instances — the output cache automatically uses whichever store is active.
import master, { RedisCacheStore } from 'mastercontroller';
// IDistributedCache / IMemoryCache
await master.cache.set('settings', value, { ttlMs: 60_000 });
await master.cache.set('session:42', value, { slidingMs: 15 * 60_000, tags: ['sessions'] });
const v = await master.cache.get('settings');
await master.cache.remove('settings');
await master.cache.evictByTag('sessions');
await master.cache.clear();
// single-flight: concurrent callers share one factory run
const cats = await master.cache.getOrCreate('categories', () => db.Category.toList(), { ttlMs: 60_000, tags: ['catalog'] });
// Redis — the output cache uses the same store
master.cache.use(new RedisCacheStore(ioredisClient, { prefix: 'app:' }));DistributedCache, MemoryCacheStore, RedisCacheStore, and OutputCache are exported from mastercontroller. For sessions, rate limiting, and CSRF stores on Redis see Monitoring & Scaling.