Skip to content

Repository files navigation

@prsm/cache

@prsm/cache

testnpm

Distributed read-through cache for Redis. Single-flight loader execution across instances, stale-while-revalidate, tag-based invalidation, negative and error caching.

Install

npm install @prsm/cache

The pattern

import{createCache}from'@prsm/cache'constcache=createCache({redis: {host: '127.0.0.1',port: 6379},defaultTtl: '5m',})constuser=awaitcache.fetch(`user:${id}`,async()=>{returnawaitdb.users.findById(id)},{ttl: '5m'})

The loader is called only on a miss. When N instances race for the same missing key, only one of them runs the loader. The rest wait for the result and receive it from Redis pub/sub. Your database sees one query, not N.

Why not just use Redis directly?

The pattern most teams write looks correct but causes outages under load:

constcached=awaitredis.get(`user:${id}`)if(cached)returnJSON.parse(cached)constuser=awaitdb.users.findById(id)awaitredis.setex(`user:${id}`,300,JSON.stringify(user))returnuser

When a hot key expires and 800 concurrent requests arrive in the same millisecond, all 800 see a miss, all 800 call the database. The database melts. This is the cache stampede problem.

This package handles it by acquiring a per-key lock when a miss happens. The lock winner runs the loader, writes the value, and publishes the result. Everyone else waits on the pub/sub channel and receives the value when the leader finishes. One database query for N concurrent requests.

Stale-while-revalidate

When staleWhile is set, a key past its TTL but still within the stale window is served immediately while a background refresh runs. Requests never block on cache regeneration.

consttrending=awaitcache.fetch('trending',loadTrending,{ttl: '1m',// fresh windowstaleWhile: '10m',// serve stale up to 10 minutes past TTL while refreshing})

Tag-based invalidation

Keys can carry tags. Invalidating a tag wipes every key that was set with it.

awaitcache.set(`order:${id}`,order,{ttl: '1h',tags: [`user:${order.userId}`,`org:${order.orgId}`],})awaitcache.invalidateTag(`user:42`)// removes every key tagged user:42

Negative and error caching

constprofile=awaitcache.fetch(`profile:${id}`,loadProfile,{ttl: '10m',negativeTtl: '30s',// cache nulls for 30s so missing-user lookups don't hit the DB forevererrorTtl: '5s',// briefly cache thrown errors so an outage doesn't cascade})

undefined returns are never cached. null is cached using negativeTtl if it is set.

API

createCache(options)

constcache=createCache({redis: { host, port, password, db, url },// or a node-redis clientprefix: 'cache:',defaultTtl: '5m',defaultStaleWhile: 0,defaultNegativeTtl: null,defaultErrorTtl: 0,defaultLockTtl: '30s',waitTimeout: '10s',serialize: JSON.stringify,deserialize: JSON.parse,})

cache.fetch(key, loader, options?)

Returns the cached value, or runs loader if missing. Options override defaults: ttl, staleWhile, negativeTtl, errorTtl, lockTtl, waitTimeout, tags.

cache.get(key)

Returns the value if the key is fresh, otherwise undefined. Does not call any loader.

cache.set(key, value, options?)

Writes a value directly. Options: ttl, staleWhile, tags.

cache.del(key)

Removes a key and its tag membership. Returns true if a key was removed.

cache.has(key)

Returns true if the key is fresh.

cache.invalidateTag(tag)

Removes every key associated with the tag. Returns the number of keys removed.

cache.stats()

Returns a snapshot of counters: hits, misses, sets, dels, errors, refreshes, stampedeLeads, stampedeWaits, stampedeSavings, invalidations.

cache.on(event, handler)

Events: hit, miss, set, del, invalidate, stampede:lead, stampede:wait, stampede:result, stampede:timeout, refresh, error.

cache.close()

Closes the underlying Redis connections.

Dev

make up # start Redis
make test # run tests
make down # stop Redis

Redis must be running on localhost:6379 for tests.

About

Distributed cache with stampede prevention, stale-while-revalidate, and tag invalidation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages