MasterController

Caching & Compression

Compress responses, cache whole responses by policy, and share a key/value cache across the app.

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 CoreMasterController
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.EvictByTagAsyncawait master.outputCache.evictByTag('posts')
IDistributedCache / IMemoryCachemaster.cache.get / set / remove / getOrCreate / evictByTag / clear
Redis distributed cachemaster.cache.use(new RedisCacheStore(ioredisClient, { prefix }))

At a glance#

javascript
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#

javascript
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#

javascript
// 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, only 200 responses, bodies up to maxBodyBytes (1 MB).
  • Not when the request is authenticated (Authorization header or this.user) unless cacheAuthenticated: true or varyByUser.
  • 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|MISS and Age.

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.

javascript
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:' }));
Named exports
DistributedCache, MemoryCacheStore, RedisCacheStore, and OutputCache are exported from mastercontroller. For sessions, rate limiting, and CSRF stores on Redis see Monitoring & Scaling.