Cacher Middleware
The Cacher Middleware is built-in middleware that gives you capability to cache the results of queries. To speed up your application. It’s only available for QueryBus
How to use it
As for any Middleware, you can use it by adding it to the bus instance.
const queryBus = createQueryBus<QueryHandlerRegistry>();queryBus.useCacherMiddleware({ adapter, cache: 'all', defaultTtl: 300 defaultStaleTtl: 60,})Remember built-in middlewares are intent aware, therefore you can customize the behavior per intent using the key
intents.
Of course, the key is in the adapter that you will provide to the createCacherMiddleware function.
This adapter must respect the CacherAdapter interface.
Here is an example:
const createMemoryCacheAdapter = (): CacherAdapter => { const memory = new Map<string, { value: unknown; expiresAt: number }>(); return { get: async (key) => { const entry = memory.get(key); if (entry && Date.now() < entry.expiresAt) { return entry.value; } memory.delete(key); return null; }, set: async (key: string, value, ttl) => { const expiresAt = Date.now() + ttl * 1000; memory.set(key, { value, expiresAt }); }, };};const adapter = createMemoryCache();Explanation
With the Cacher Middleware, if the intent result is already in the cache, it will be returned directly without calling the handler.
Aside of the adapter, you can provide the following parameters to the createCacherMiddleware function (and per intents):
- cache:
alloronly-cacheable(default:all) -allwill cache all results - defaultTtl: number (default:
3600) - the default time to live in seconds - defaultStaleTtl: number (default:
60) - the default time to return stale data in seconds
Cache on demand
If you want to control the cacheable state from the handler (or another middleware), you can pass the option cache equals to only-cacheable. (globally or per intent)
Then you need to tell the Middleware which queries are cacheable by adding the cacheable stamp to the result.
const handler = async (envelope: Envelope<Query>, deps: Deps) => { // your handler code here envelope.addStamp<CacheableStamp>('missive:cacheable', { ttl: 1800, staleTtl: 60 }); return { // your result here }};Added Stamps
The Cacher Middleware is going to add:
-
type FromCacheStamp = Stamp<{ age: number; stale: boolean }, 'missive:cache:hit'>;
When the result is coming from cache.
-
type HandledStamp<R> = Stamp<R, 'missive:handled'>;
With the result from the cache. (this stamp will always be there, added by this middleware or the handler)
Shortcircuiting the processing
As explain the Middleware guide, you can shortcircuit the processing by NOT calling next() in a middleware.
This is exactly what the CacherMiddleware does. It does not call next() which means that all the middlewares after it will be skipped.
You can change this behavior by providing the shortCircuit flag to false.
queryBus.useCacherMiddleware({ adapter: memoryStorage, shortCircuit: false, // default is true defaultTtl: 20, intents: { ListAllCharacters: { shortCircuit: true }, // this intent will skip all the middlewares after Cacher }, });With great power comes great responsibility. - Missive.js is not opinionated, it’s up to you to decide what is best for your application.
Stale While Revalidating
The Cacher Middleware is using the Stale While Revalidating pattern by redispatching the query to the bus behind the scene to refresh the cache.
It works like this:
-
While using the
useCacherMiddlewaremethod on the QueryBus, thecreateCacherMiddlewareis called with the options and with theQueryBusinstance.It does mean the middleware has access to the bus and can redispatch the intent.
-
When the result is coming from the cache, the middleware will detect if the data is
staleor not.It does mean that the middleware cache for
ttlseconds +staleTtlseconds in the cache store (via the adapter). -
If the data is stale, the middleware will redispatch the
queryto the bus in a fresh new Envelope. Thequerywill then go through the whole chain of middleware(s) and handler(s), and the result will be cached again.The Cacher Middleware knows to avoid the cache lookup thanks to the
ReprocessedStampadded by the Envelope when thequeryis redispatched.
This pattern achieves the best of both worlds: speed and freshness.
You can disable this behavior by setting the staleTtl to 0.
Going further
Missive.js. MIT License.
Powered by Astro Starlight.
Inspired by Symfony Messenger