@disckit/caffeine
v1.3.0
Published
Caffeine-inspired async cache builder. expireAfterWrite, expireAfterAccess, refreshAfterWrite, request coalescing.
Maintainers
Readme
Features
- Builder pattern —
CacheBuilder.newBuilder()...buildAsync(loader) expireAfterWrite— entries expire N ms after last writeexpireAfterAccess— entries expire N ms after last read (idle timeout)refreshAfterWrite— return stale value immediately, reload in backgroundonEviction— callback fired on every eviction withreason: 'size' | 'expired' | 'manual'- Request coalescing — concurrent
get()calls for the same key → 1 loader call - Stats — hits, misses, loads, errors, evictions, refreshes
- Full TypeScript types included · Zero dependencies · Node.js 18+
Inspired by the Java Caffeine library used by Loritta.
Installation
npm install @disckit/caffeine
yarn add @disckit/caffeine
pnpm add @disckit/caffeineTypeScript / ESM
Types are bundled — no extra install needed.
Supports both CommonJS and ESM:
// ESM
import { CacheBuilder, CaffeineCache } from '@disckit/caffeine';
// CommonJS
const { CacheBuilder } = require('@disckit/caffeine');Usage
Guild settings cache (most common use case)
const { CacheBuilder } = require('@disckit/caffeine');
const guildCache = CacheBuilder.newBuilder()
.maximumSize(500)
.expireAfterAccess(30 * 60 * 1000) // evict guilds idle for 30 min
.refreshAfterWrite(5 * 60 * 1000) // background refresh every 5 min
.onEviction((key, value, reason) => {
console.log(`Guild ${key} evicted: ${reason}`);
})
.buildAsync(async (guildId) => {
let doc = await Guild.findById(guildId);
if (!doc) doc = await new Guild({ _id: guildId }).save();
return doc;
});
// Usage in commands:
const settings = await guildCache.get(interaction.guildId);
// After a dashboard save:
guildCache.invalidate(guildId);Member stats cache
const memberCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10 * 60 * 1000) // expire 10 min after last write
.buildAsync(async (key) => {
const [guildId, memberId] = key.split('|');
return MemberStats.findOne({ guild_id: guildId, member_id: memberId });
});
const stats = await memberCache.get(`${guildId}|${memberId}`);
stats.xp += 10;
await stats.save();
memberCache.invalidate(`${guildId}|${memberId}`); // bust cache after savePer-call loader (no bound loader)
const cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(60_000)
.build(); // no bound loader
// Pass loader on each call:
const data = await cache.get('api-response', async (key) => {
return fetch(`https://api.example.com/${key}`).then(r => r.json());
});Manual writes with put()
// Pre-warm the cache or write without loading
cache.put('guild:123', { prefix: '!', lang: 'pt' });
cache.getIfPresent('guild:123'); // → { prefix: '!', lang: 'pt' }API Reference
CacheBuilder.newBuilder()
| Method | Description |
|--------|-------------|
| .maximumSize(n) | Max entries before LRU eviction |
| .expireAfterWrite(ms) | Expire N ms after last write |
| .expireAfterAccess(ms) | Expire N ms after last read |
| .refreshAfterWrite(ms) | Background refresh after N ms (stale-while-revalidate) |
| .onEviction(fn) | (key, value, reason) => void callback |
| .build() | Build without bound loader — pass loader per get() call |
| .buildAsync(loader) | Build with bound async (key) => value loader |
CaffeineCache
| Method | Description |
|--------|-------------|
| get(key, loader?) | Get value, loading if missing or expired |
| put(key, value) | Manual write |
| getIfPresent(key) | Get only if cached and not expired |
| has(key) | Check existence |
| invalidate(key) | Remove one key |
| invalidateAll() | Clear entire cache |
| cleanUp() | Proactively evict expired entries |
| stats | { hits, misses, loads, errors, evictions, refreshes, size, inflight } |
