cache
Remember slow results on the server and clear them by tag when the data changes.
Some reads are slow or expensive: a report that joins six tables, a call to a rate-limited API, a page of Markdown rendered from a CMS. kitr/cache keeps the result for a while so the next request gets it instantly. When the data changes, you clear every cached result about it with one tag instead of hunting down keys.
It runs on the server only. In the browser, query already caches what the API returns.
Install
kitr add cacheFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
This writes config/cache.ts and registers the provider, which binds cache in the container:
// config/cache.ts
import { defineConfig } from 'kitr/cache'
export default defineConfig({
prefix: 'my-app:',
defaultTtl: 300,
})
| Option | Default | Description |
|---|---|---|
driver |
memory | Where entries live. See Drivers. |
prefix |
none | Prepended to every key, so several apps can share one Redis. |
defaultTtl |
300 |
Seconds an entry lives when you don't pass a ttl. |
Remember a result
remember(key, fn, options) returns the cached value, or runs fn, stores what it returns and returns that:
// server/controllers/reports_controller.ts
import type { HttpContext } from 'kitr/api'
import { useCache } from 'kitr/cache'
export default class ReportsController {
async show({ app, params }: HttpContext<{ id: string }>) {
return useCache(app).remember(
`report:${params.id}`,
() => buildReport(params.id), // the slow part
{ ttl: 600, tags: ['reports', `project:${params.id}`] },
)
}
}
If fifty requests miss at the same moment, fn runs once and all fifty get its result. A function that returns undefined isn't cached, so it's asked again next time.
Clear by tag
Tag entries with what they're about, then clear the tag after a write:
async update({ app, params, body }: HttpContext<{ id: string }>) {
const project = await db.projects.update(params.id, body)
await useCache(app).invalidateTags(`project:${params.id}`)
return project
}
Every entry tagged project:42 is gone, whatever its key was. invalidateTags('a', 'b') clears several tags at once.
Wrap a function
cached() turns a function into a cached one. Each distinct argument list gets its own entry:
import { cached, useCache } from 'kitr/cache'
const exchangeRate = cached(
useCache(app),
(from: string, to: string) => ratesApi.get(from, to),
{ key: (from, to) => `rate:${from}:${to}`, ttl: 3600 },
)
await exchangeRate('EUR', 'USD')
Reading and writing directly
| Method | Description |
|---|---|
get(key) |
The value, or undefined on a miss. |
set(key, value, { ttl, tags }) |
Store a value. Values are stored as JSON. |
has(key) |
Whether a live entry exists. |
delete(key) |
Remove one entry. |
clear() |
Remove everything. |
Drivers
The memory driver is the default. It's fast and needs nothing, but each server process has its own copy and it empties on restart. Once you run more than one instance, use a shared store.
A driver is an object with six methods over strings, so Redis takes a few lines:
// config/cache.ts
import { defineConfig, type CacheDriver } from 'kitr/cache'
import { Redis } from 'ioredis'
const redis = new Redis(process.env.REDIS_URL!)
const redisDriver: CacheDriver = {
name: 'redis',
get: async (key) => (await redis.get(key)) ?? undefined,
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, 'EX', ttl)
else await redis.set(key, value)
},
delete: async (key) => void (await redis.del(key)),
tag: async (key, tags) => void (await Promise.all(tags.map((t) => redis.sadd(`tag:${t}`, key)))),
invalidateTags: async (tags) => {
for (const t of tags) {
const keys = await redis.smembers(`tag:${t}`)
if (keys.length) await redis.del(...keys)
await redis.del(`tag:${t}`)
}
},
clear: async () => void (await redis.flushdb()),
}
export default defineConfig({ driver: redisDriver, prefix: 'my-app:' })
Tests
createCache() builds a cache with the memory driver and no app, which is all a test needs:
import { createCache } from 'kitr/cache'
const cache = createCache({ defaultTtl: 60 })