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();

Auto Cache Keys

When a wrapped function receives arguments, Cachetta automatically generates unique cache paths by hashing the arguments:

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

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

await getUser(1);   // cached at ./cache/users-<hash1>.json
await getUser(2);   // cached at ./cache/users-<hash2>.json

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
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