Rendered docs: thekevinscott.github.io/cachetta/javascript

Cachetta for TypeScript

File-based caching for TypeScript. Uses v8.serialize for native binary serialization – any file extension works, and all V8-serializable types (Maps, Sets, Dates, Buffers, typed arrays, RegExps, etc.) are supported natively.

Install

pnpm add cachetta

Basic Usage

Create a cache object:

import { Cachetta } from 'cachetta';

const cache = new Cachetta({
  read: true,
  write: true,
  path: './cache.json',
  duration: 24 * 60 * 60 * 1000, // 1 day in milliseconds
});

Read and write:

import { readCache, writeCache } from 'cachetta';

async function getData() {
  const cachedData = await readCache(cache);
  if (cachedData) {
    return cachedData;
  }
  const data = await fetchData();
  await writeCache(cache, data);
  return data;
}

Decorators

Use Cachetta as a decorator (requires experimental decorators):

import { Cachetta } from 'cachetta';

class DataService {
  @Cachetta({ path: '/my-cache.json' })
  async getData() {
    return await fetchData();
  }
}

With a specific cache object:

const cache = new Cachetta({ path: '/my-cache.json' });

class DataService {
  @cache
  async getData() {
    return await fetchData();
  }
}

Or with overrides:

const cache = new Cachetta({ path: '/my-cache.json' });

class DataService {
  @cache({ duration: 1000 })
  async getData() {
    return await fetchData();
  }
}

Decorated functions always return Promises, even if the original function is synchronous. Always use await when calling decorated functions.

Function Wrapper

If you’re not using decorators, wrap functions manually:

const cache = new Cachetta({ path: './my-cache.json' });

const cachedGetData = cache(async () => {
  return await fetchData();
});

const result = await cachedGetData();

With configuration:

const cache = new Cachetta({ path: './cache' });

const cachedGetData = cache(getData, {
  path: (id) => `./cache/data-${id}.json`,
  duration: 5000
});

const result = await cachedGetData(123);

Sync API

All methods have synchronous counterparts:

import { Cachetta, writeCacheSync, readCacheSync } from 'cachetta';

const cache = new Cachetta({ path: './cache.json' });

// Sync read/write
writeCacheSync(cache, { data: 1 });
const data = readCacheSync(cache);

// Sync inspection
cache.existsSync();
cache.ageSync();
cache.infoSync();

// Sync invalidation
cache.invalidateSync();

// Sync function wrapping
const cachedFn = cache.wrapSync(() => computeExpensiveValue());
const result = cachedFn();

Per-Argument Cache Files

A string path is used verbatim — every call writes to the same file regardless of arguments. To key cache files by argument, pass path as a function that receives the wrapped function’s arguments:

const cache = new Cachetta({ path: (userId) => `./cache/users/${userId}.json` });

const getUser = cache((userId) => fetchUser(userId));

await getUser(1);   // cached at ./cache/users/1.json
await getUser(2);   // cached at ./cache/users/2.json

Hashed mode

When you want one file per arg-set inside a folder (the common LLM / embedding cache shape), set hashed: true. The path you pass is treated as a directory, and entries are written as {path}/{hash(...args)}:

const cache = new Cachetta({ path: './cache/llm', hashed: true });

const call = cache((prompt) => llm(prompt));

await call('hello');   // ./cache/llm/<hash>
await call('world');   // ./cache/llm/<otherhash>

hashed is a regular field on CacheConfig, so it works at every entrypoint:

// Constructor
const cache = new Cachetta({ path: './cache', hashed: true });

// Per-wrap override (creates an isolated copy, base cache is not mutated)
const cached = baseCache(fn, { hashed: true });

// Copy
const hashedCache = baseCache.copy({ hashed: true });

If path is a callable, it picks the folder and the hash names the file within it — the “shard by one arg, hash by all” pattern:

const cache = new Cachetta({
  path: (model, prompt) => `./cache/${model}`,
  hashed: true,
});

const callLLM = cache(async (model, prompt) => callApi(model, prompt));

await callLLM('gpt', 'hi');      // ./cache/gpt/<hash('gpt', 'hi')>
await callLLM('claude', 'hi');   // ./cache/claude/<hash('claude', 'hi')>

hashed composes with condition and the LRU.

Public hash helper

The same digest the auto-keyed path uses is exposed as a top-level hash export. Use it when you want to construct cache paths manually (e.g. inside a path: callable that keys on a subset of args) and keep them aligned with cachetta’s own keying:

import { Cachetta, hash } from 'cachetta';

const cache = new Cachetta({
  path: (model, prompt, opts) => `./cache/llm/${model}/${hash(prompt)}.json`,
});

const callLLM = cache(async (model, prompt, opts) => callApi(model, prompt, opts));

hash(...args) accepts any JSON-serializable arguments and returns a 16-char hex string. It’s a pure function — no I/O, no Cachetta instance required.

The JS and Python hash exports are not cross-language portable. They use different stringifiers (JSON.stringify vs json.dumps(..., default=str)) and the Python variant also folds in **kwargs, so the same logical input produces different digests in each language. Use each language’s hash only to align with that language’s own cachetta.

In-Memory LRU

Add an in-memory LRU layer that is checked before hitting disk:

const cache = new Cachetta({
  path: './cache.json',
  lruSize: 100,
});

LRU entries respect the same duration as disk entries and use lazy expiration.

Conditional Caching

Cache results only when a condition function returns true:

const cache = new Cachetta({
  path: './cache.json',
  condition: (result) => result !== null,
});

Stale-While-Revalidate

Return expired data immediately while refreshing in the background:

const cache = new Cachetta({
  path: './cache.json',
  duration: 60 * 60 * 1000,        // 1 hour
  staleDuration: 30 * 60 * 1000,   // serve stale up to 30min past expiry
});

Cache Invalidation

const cache = new Cachetta({ path: './cache.json' });

await cache.invalidate();  // or cache.clear()
cache.invalidateSync();    // sync variant

// With arguments (when using path functions)
await cache.invalidate('userId');

Cache Inspection

Query cache state without reading the cached data:

const cache = new Cachetta({ path: './cache.json' });

await cache.exists();   // true if the cache file exists
await cache.age();      // age in milliseconds, or null
await cache.info();     // { exists, age, expired, stale, path }

// Sync variants
cache.existsSync();
cache.ageSync();
cache.infoSync();

Dynamic Cache Paths

Specify a function for defining the path:

function getCachePath(n) {
  return `./cache/${n}.json`;
}

@Cachetta({ path: getCachePath })
async function foo(n) {
  return computeExpensiveValue(n);
}

Specifying Paths

Use copy to create variations of a cache configuration:

const cache = new Cachetta({ path: './cache' });

const newCache = cache.copy({
  read: false,
  duration: 2 * 24 * 60 * 60 * 1000,
});

Error Handling

Cachetta gracefully handles corrupt cache files by returning null:

const cache = new Cachetta({ path: './cache.json' });

const data = await readCache(cache);
if (data === null) {
  // Cache is missing or corrupt
  const freshData = await fetchFreshData();
  await writeCache(cache, freshData);
}

Logging

import { setLogLevel, setLogger } from 'cachetta';

// Enable debug logging
setLogLevel('debug');  // 'error', 'warn', 'info', 'debug'

// Or use a custom logger
setLogger({
  debug: (msg) => myLogger.debug(msg),
  info: (msg) => myLogger.info(msg),
  warn: (msg) => myLogger.warn(msg),
  error: (msg) => myLogger.error(msg),
});

Configuration Reference

Option Type Default Description
path string \| Function required Cache file path or path function
hashed boolean false Treat path as a folder; write one file per arg-hash inside it
read boolean true Allow reading from cache
write boolean true Allow writing to cache
duration number 7 days (ms) Cache TTL in milliseconds
lruSize number undefined Max in-memory LRU entries
condition Function undefined Predicate to decide whether to cache
staleDuration number undefined Time past expiry to serve stale data