News

TriFrost 0.8.0

Wednesday, May 14, 2025|peterver

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: @cache decorator β€” 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: cacheFn function β€” 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 in cacheSkip().

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: @span and spanFn now support this.ctx.logger as a fallback if neither ctx.logger nor this.logger is 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.delete has been renamed to ctx.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');