A high-performance, production-ready Hybrid L1/L2 Caching Suite for Node.js. Designed for speed, reliability, and developer experience.
- π Hybrid Architecture: Blazing fast Local Memory (L1) fallback to Distributed Redis (L2).
- π Stale-While-Revalidate (SWR): Serve stale data instantly while refreshing in the background.
- π‘οΈ Stampede Protection: Prevents "Thundering Herd" by collapsing concurrent duplicate requests.
- π‘ Event-Driven: Built-in EventEmitter for monitoring hits, misses, expirations, and SWR triggers.
- π¦ Batch Operations: Atomic-like pipelines for
mget,mset, andmdel. - π οΈ Custom Serialization: Plug-in support for
SuperJSON,msgpack, or any custom format. - π§Ή Pattern Clearing: Efficiently clear keys using glob patterns (e.g.,
user:*). - π·οΈ Tag-Based Invalidation: Group keys with tags and invalidate entire categories at once.
- π Metrics: Built-in tracking for hits, misses, deletes, and hit rate via
cache.metrics.get().
pnpm add @explita/cache ioredis
# or
npm install @explita/cache ioredisTo enable the Redis (L2) layer, simply install ioredis and set the following environment variables in your .env file:
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=optional_passwordIf these variables are present, the cache will automatically attempt to connect to Redis using a default internal client, unless a custom client is provided in the configuration.
import { createCache } from "@explita/cache";
const cache = createCache("app:", {
enableL1: true, // Enable local memory cache
l1Ttl: "1m", // L1 TTL (shorter than L2)
});
// Basic usage
await cache.set("users:1", { name: "Explita" }, { ttl: 3600 });
const user = await cache.get("users:1");
// Powerful 'remember' with SWR
const data = await cache.remember(
"profile:1",
async () => {
return await fetchProfileFromDB(1);
},
{
ttl: 60,
staleTtl: 300, // Return stale data for 5 mins while revalidating
},
);Easily handle complex types using libraries like superjson:
import superjson from "superjson";
const cache = createCache("meta:", {
serializer: (v) => superjson.stringify(v),
deserializer: (v) => superjson.parse(v),
});
await cache.set("now", new Date()); // Works perfectly!Group keys together and wipe them out in one go:
// Set data with tags
await cache.set("product:123", { name: "MacBook" }, { tags: ["products"] });
await cache.set("product:456", { name: "iPhone" }, { tags: ["products"] });
// Later, invalidate everything in the 'products' category
await cache.invalidate("products");cache.on("hit", (key, value) => console.log(`π₯ Hit: ${key}`));
cache.on("miss", (key) => console.log(`βοΈ Miss: ${key}`));
cache.on("swr", (key) => console.log(`π Refreshing: ${key}`));
cache.on("expired", (key) => console.log(`π Expired: ${key}`));You can also use the underlying stores standalone if you don't need the orchestrator:
import { createMemoryStore, createRedisStore } from "@explita/cache";
const mem = createMemoryStore({ cleanupInterval: "10s" });
const redisStore = createRedisStore(myRedisClient);| Method | Description |
|---|---|
get(key, opts?) |
Get a value. Supports sliding expiration via { touch }. |
pop(key) |
Get a value and delete it in one call. |
set(key, value, opts?) |
Set a value with optional TTL, SWR, and tags. |
setnx(key, value, opts?) |
Set only if key doesn't exist. Returns true/false. |
cas(key, expected, new, opts?) |
Atomic compare-and-swap. |
del(key) |
Delete a key. |
mget(keys) |
Get multiple values in batch. |
mset(entries, opts?) |
Set multiple values in batch. |
mdel(keys) |
Delete multiple keys. |
clear(pattern) |
Delete keys matching a glob pattern (e.g. "user:*"). |
has(key) |
Check if a key exists. |
ttl(key) |
Get remaining TTL in seconds. |
expire(key, ttl) |
Set TTL on an existing key. |
remember(key, fn, opts?) |
Get or compute + cache. Supports SWR and stampede protection. |
invalidate(tag) |
Delete all keys associated with a tag. |
keys(pattern) |
List keys matching a glob pattern. |
entries(pattern) |
Get key-value pairs matching a glob pattern. |
namespace(prefix) |
Create a scoped sub-cache with an additional prefix. |
metrics.get() |
Snapshot of hits, misses, hit rate, etc. |
metrics.reset() |
Reset all metric counters. |
destroy() |
Clean shutdown β clears intervals, disconnects Redis. |
| Option | Type | Default | Description |
|---|---|---|---|
ttl |
TTLWindow |
300 (5m) |
Time-to-live in seconds or string like "1h". |
staleTtl |
TTLWindow |
β | SWR window β serve stale data while refreshing in background. |
tags |
string[] |
[] |
Tags for bulk invalidation. |
| Option | Type | Default | Description |
|---|---|---|---|
client |
Redis |
auto | ioredis client. Auto-created from REDIS_HOST/REDIS_PORT env vars if omitted. |
enableL1 |
boolean |
false |
Enable local memory cache (L1) on top of Redis (L2). |
l1Ttl |
TTLWindow |
"1m" |
TTL for L1 entries (shorter than L2). |
l1MaxKeys |
number |
β | Max L1 keys before LRU eviction. |
touch |
TTLWindow |
β | Default sliding expiration β extends TTL on every get hit. |
cleanupInterval |
TTLWindow |
60 (1m) |
How often expired L1 entries are purged. |
enableMemoryCleanup |
boolean |
true |
Enable periodic L1 cleanup interval. |
logger |
CacheLogger |
console |
Logger for info/error messages. |
loggingEnabled |
boolean |
true |
Toggle all logging. |
serializer |
(v) => string |
JSON.stringify |
Custom serializer. |
deserializer |
(s) => any |
JSON.parse |
Custom deserializer. |
@explita/cache is built to provide high-performance caching solutions with advanced features like SWR and stampede protection. If it has improved your application's speed or reliability, please consider supporting the project to ensure its continued growth and maintenance!
- Give us a β: It helps others discover the project.
- Join the Discussion: Report bugs or suggest new features.
- Spread the Word: Share your experience with @explita/cache on social media.
A huge thank you to everyone helping us build faster applications!
MIT Β© Explita