SkillAgentSearch skills...

Cachified

🤑 wrap virtually everything that can store by key to act as cache with ttl/max-age, stale-while-validate, parallel fetch protection and type-safety support

Install / Use

/learn @epicweb-dev/Cachified
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<div> <h1 align="center"><a href="https://npm.im/@epic-web/cachified">🤑 @epic-web/cachified</a></h1> <strong> A simple API to make your app faster. </strong> <p> Cachified allows you to cache values with support for time-to-live (ttl), stale-while-revalidate (swr), cache value validation, batching, and type-safety. </p> </div>
npm install @epic-web/cachified
<div align="center"> <a alt="Epic Web logo" href="https://www.epicweb.dev" > <img width="300px" src="https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/257881576-fd66040b-679f-4f25-b0d0-ab886a14909a.png" /> </a> </div> <hr /> <!-- prettier-ignore-start -->

[![Build Status][build-badge]][build] [![MIT License][license-badge]][license] [![Code of Conduct][coc-badge]][coc]

<!-- prettier-ignore-end -->

Watch the talk "Caching for Cash 🤑" on EpicWeb.dev:

Kent smiling with the cachified README on npm behind him

Install

npm install @epic-web/cachified
# yarn add @epic-web/cachified

Usage

<!-- usage-intro -->
import { LRUCache } from 'lru-cache';
import { cachified, CacheEntry, Cache, totalTtl } from '@epic-web/cachified';

/* lru cache is not part of this package but a simple non-persistent cache */
const lruInstance = new LRUCache<string, CacheEntry>({ max: 1000 });

const lru: Cache = {
  set(key, value) {
    const ttl = totalTtl(value?.metadata);
    return lruInstance.set(key, value, {
      ttl: ttl === Infinity ? undefined : ttl,
      start: value?.metadata?.createdTime,
    });
  },
  get(key) {
    return lruInstance.get(key);
  },
  delete(key) {
    return lruInstance.delete(key);
  },
};

function getUserById(userId: number) {
  return cachified({
    key: `user-${userId}`,
    cache: lru,
    async getFreshValue() {
      /* Normally we want to either use a type-safe API or `checkValue` but
         to keep this example simple we work with `any` */
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`,
      );
      return response.json();
    },
    /* 5 minutes until cache gets invalid
     * Optional, defaults to Infinity */
    ttl: 300_000,
  });
}

// Let's get through some calls of `getUserById`:

console.log(await getUserById(1));
// > logs the user with ID 1
// Cache was empty, `getFreshValue` got invoked and fetched the user-data that
// is now cached for 5 minutes

// 2 minutes later
console.log(await getUserById(1));
// > logs the exact same user-data
// Cache was filled an valid. `getFreshValue` was not invoked

// 10 minutes later
console.log(await getUserById(1));
// > logs the user with ID 1 that might have updated fields
// Cache timed out, `getFreshValue` got invoked to fetch a fresh copy of the user
// that now replaces current cache entry and is cached for 5 minutes

Options

<!-- ignore -->
interface CachifiedOptions<Value> {
  /**
   * Required
   *
   * The key this value is cached by
   * Must be unique for each value
   */
  key: string;
  /**
   * Required
   *
   * Cache implementation to use
   *
   * Must conform with signature
   *  - set(key: string, value: object): void | Promise<void>
   *  - get(key: string): object | Promise<object>
   *  - delete(key: string): void | Promise<void>
   */
  cache: Cache;
  /**
   * Required
   *
   * Function that is called when no valid value is in cache for given key
   * Basically what we would do if we wouldn't use a cache
   *
   * Can be async and must return fresh value or throw
   *
   * receives context object as argument
   *  - context.metadata.ttl?: number
   *  - context.metadata.swr?: number
   *  - context.metadata.createdTime: number
   *  - context.background: boolean
   */
  getFreshValue: GetFreshValue<Value>;
  /**
   * Time To Live; often also referred to as max age
   *
   * Amount of milliseconds the value should stay in cache
   * before we get a fresh one
   *
   * Setting any negative value will disable caching
   * Can be infinite
   *
   * Default: `Infinity`
   */
  ttl?: number;
  /**
   * Amount of milliseconds that a value with exceeded ttl is still returned
   * while a fresh value is refreshed in the background
   *
   * Should be positive, can be infinite
   *
   * Default: `0`
   */
  staleWhileRevalidate?: number;
  /**
   * Alias for staleWhileRevalidate
   */
  swr?: number;
  /**
   * Validator that checks every cached and fresh value to ensure type safety
   *
   * Can be a standard schema validator or a custom validator function
   * @see https://github.com/standard-schema/standard-schema?tab=readme-ov-file#what-schema-libraries-implement-the-spec
   *
   * Value considered ok when:
   *  - schema succeeds
   *  - validator returns
   *    - true
   *    - migrate(newValue)
   *    - undefined
   *    - null
   *
   * Value considered bad when:
   *  - schema throws
   *  - validator:
   *    - returns false
   *    - returns reason as string
   *    - throws
   *
   * A validator function receives two arguments:
   *  1. the value
   *  2. a migrate callback, see https://github.com/epicweb-dev/cachified#migrating-values
   *
   * Default: `undefined` - no validation
   */
  checkValue?:
    | CheckValue<Value>
    | StandardSchemaV1<unknown, Value>
    | Schema<Value, unknown>;
  /**
   * Set true to not even try reading the currently cached value
   *
   * Will write new value to cache even when cached value is
   * still valid.
   *
   * Default: `false`
   */
  forceFresh?: boolean;
  /**
   * Whether or not to fall back to cache when getting a forced fresh value
   * fails
   *
   * Can also be a positive number as the maximum age in milliseconds that a
   * fallback value might have
   *
   * Default: `Infinity`
   */
  fallbackToCache?: boolean | number;
  /**
   * Promises passed to `waitUntil` represent background tasks which must be
   * completed before the server can shutdown. e.g. swr cache revalidation
   *
   * Useful for serverless environments such as Cloudflare Workers.
   *
   * Default: `undefined`
   */
  waitUntil?: (promise: Promise<unknown>) => void;
  /**
   * Trace ID for debugging, is stored along cache metadata and can be accessed
   * in `getFreshValue` and reporter
   */
  traceId?: any;
}

Adapters

There are some adapters available for common caches. Using them makes sure the used caches cleanup outdated values themselves.

Advanced Usage

Stale while revalidate

Specify a time window in which a cached value is returned even though it's ttl is exceeded while the cache is updated in the background for the next call.

<!-- stale-while-revalidate -->
import { cachified } from '@epic-web/cachified';

const cache = new Map();

function getUserById(userId: number) {
  return cachified({
    ttl: 120_000 /* Two minutes */,
    staleWhileRevalidate: 300_000 /* Five minutes */,

    cache,
    key: `user-${userId}`,
    async getFreshValue() {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`,
      );
      return response.json();
    },
  });
}

console.log(await getUserById(1));
// > logs the user with ID 1
// Cache is empty, `getFreshValue` gets invoked and and its value returned and
// cached for 7 minutes total. After 2 minutes the cache will start refreshing in background

// 30 seconds later
console.log(await getUserById(1));
// > logs the exact same user-data
// Cache is filled an valid. `getFreshValue` is not invoked, cached value is returned

// 4 minutes later
console.log(await getUserById(1));
// > logs the exact same user-data
// Cache timed out but stale while revalidate is not exceeded.
// cached value is returned immediately, `getFreshValue` gets invoked in the
// background and its value is cached for the next 7 minutes

// 30 seconds later
console.log(await getUserById(1));
// > logs fresh user-data from the previous call
// Cache is filled an valid. `getFreshValue` is not invoked, cached value is returned

Forcing fresh values and falling back to cache

We can use forceFresh to get a fresh value regardless of the values ttl or stale while validate

<!-- force-fresh -->
import { cachified } from '@epic-web/cachified';

const cache = new Map();

function getUserById(userId: number, forceFresh?: boolean) {
  return cachified({
    forceFresh,
    /* when getting a forced fresh value fails we fall back to cached value
       as long as it's not older then 5 minutes */
    fallbackToCache: 300_000 /* 5 minutes, defaults to Infinity */,

    cache,
    key: `user-${userId}`,
    async getFreshValue() {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`,
      );
      return response.json();
    },
  });
}

console.log(await getUserById(1));
// > logs the user with ID 1
// Cache is empty, `getFreshValue` gets invoked and and its value returned

console.log(await getUserById(1, true));
// > logs fresh user with ID 1
// Cache is filled an valid. but we forced a fresh value, so `getFreshValue` is invoked

Type-safety

In practice we can not be entirely sure that values

View on GitHub
GitHub Stars1.1k
CategoryCustomer
Updated2d ago
Forks28

Languages

TypeScript

Security Score

100/100

Audited on Mar 26, 2026

No findings