TriFrost now ships with a caching system that’s not only powerful — but invisible. 🪄
Caching is one of those things you want to do wherever possible: inside services, on expensive lookups, even conditional branches. But until now, you had two options:
1. Write your own cache keys and wrap logic manually — again and again (think very generic boilerplate).
2. Forget to cache at all.
Let’s fix that.
Added
- feat:
@cachedecorator — Automatically wraps your method in cache logic:
import {cache} from '@trifrost/core';
class Releases {
@cache('releases')
async list(ctx: Context) {
return fetchFromUpstream();
}
/* Supports dynamic keys via ctx (ps: the function you pass will be typed based on the context it gets) */
@cache(ctx => `release:${ctx.state.id}`)
async one <State extends {id:string}> (ctx:Context<State>) {
return fetchRelease(ctx.state.id);
}
}- feat:
cacheFnfunction — Wrap standalone or arrow functions with cache logic:
import {cacheFn} from '@trifrost/core';
const getReleases = cacheFn('releases', (ctx) => fetchFromUpstream(...));
const getRelease = cacheFn <State extends {id:string}> (
ctx => `release:${ctx.state.id}`,
(ctx:Context<State>) => fetchRelease(ctx.state.id)
);- feat:
cacheSkip()— Want to bail from caching? Just return your result wrapped incacheSkip().
Works when manually using cache wrap:
export async function getReleases (ctx:Context) {
return ctx.cache.wrap('myKey', async () => {
try {
const data = await maybeFails();
return data;
} catch (err) {
ctx.logger.error(err);
return cacheSkip(null);
}
});
}Works within @cache decorated methods:
import {cacheSkip} from '@trifrost/core';
class Releases {
@cache('releases')
async getReleases(ctx: Context) {
try {
...
return fetchFromUpstream();
} catch (err) {
ctx.logger.error(err);
return cacheSkip(null);
}
}
}Works within cacheFn wrapped methods:
import {cacheFn, cacheSkip} from '@trifrost/core';
const getRelease = cacheFn('getRelease', async (ctx:Context) => {
try {
return await fetchRelease(ctx.state.id);
} catch (err) {
ctx.logger.error(err);
return cacheSkip(null);
}
});Improved
- feat: Caches now accept primitives as values —
null,true,false,0,"hello", etc. No need to always wrap things in objects. - feat:
@spanandspanFnnow supportthis.ctx.loggeras a fallback if neitherctx.loggernorthis.loggeris available.
class Releases {
constructor(ctx: Context) {
this.ctx = ctx;
}
@span()
@cache('releases')
async getReleases() {
return fetchFromUpstream();
}
}No ctx needed — both @span and @cache find what they need.
- deps: Upgrade @cloudflare/workers-types to 4.20250514.0
- deps: Upgrade @types/node to 22.15.18
- deps: Upgrade typescript-eslint to 8.32.1
Breaking
- feat:
ctx.cache.deletehas been renamed toctx.cache.del. This saves 4 keystrokes 🚀 and aligns with the rest of the ecosystem:
ctx.cookies.del('token');
ctx.router.del('/route', handler);
ctx.cache.del('myKey');