@playstack/cache
JSON caching with millisecond TTLs, tag generations, stampede-safe reads, and explicit capabilities.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/cache provides a small exact-JSON cache with millisecond TTLs, canonical tag generations, stampede-safe remember, explicit capabilities, and native client access.
Install
After confirming preview access, install the package at your application's shared Playstack version:
npm install --save-exact @playstack/cache@0.1.0-beta.1Check the peer requirements below before choosing a runtime or provider.
Compose and use
import { createCache } from '@playstack/cache'
import { RedisCacheStorage } from '@playstack/cache/redis'
const cache = createCache({
app: 'billing',
environment: 'production',
driver: new RedisCacheStorage(redis),
lockTtlMs: 30_000,
})
const plan = await cache.remember('plans:account-1', 60_000, () => loadPlan())
const accountCache = cache.tags('accounts', 'region:us')
await accountCache.put('summary', { total: 4 }, 60_000)
await accountCache.flush()The physical namespace is {app}:{environment}:cache. Keys allow up to 512 UTF-8 bytes and no control characters. TTLs are positive integer milliseconds.
Configuration reference
| Property | Required | Purpose |
|---|---|---|
app, environment | Yes | Canonical physical namespace. |
driver | Yes | JSON entry, tag-generation, and optional lock bridge. |
lockTtlMs | No | Distributed stampede lease; defaults to 30 seconds. |
Values cross a versioned exact-JSON boundary. Class instances, undefined, non-finite numbers, sparse arrays, symbols, and cycles fail before the driver. Cached null is retained by remember, while get<T>() intentionally uses T | null for conventional misses.
Tag and lock behavior
Tags are sorted and deduplicated. Tagged and untagged keys remain separate. flush() increments canonical tag generations, making old entries unreachable without scanning; they expire on their original TTL.
Concurrent remember calls on one cache instance always share a promise. A driver with atomic locks extends that across processes. Loader errors remain unchanged, are not cached, and release the lease.
Native lock-capable storage fences publication with putIfLockOwner: a loader whose lease expired cannot overwrite a successor's cached value. Its original caller can still receive the computed, uncached result; the loader is not automatically rerun. This is not an exactly-once execution guarantee.
Batches, bounded memory, and observations
getMany<T>(keys) returns ordered values or nulls and preserves duplicate read keys. putMany([{ key, value, ttlMs }]) validates and snapshots the whole batch before I/O and rejects duplicate write keys. Batches must be dense arrays of at most 1,000 entries; empty batches are no-ops. Tagged batches use one generation snapshot. Redis uses a native multi-get or Lua write; other drivers can fall back to single-key operations. A failed write may be partial or unacknowledged: there is no portable batch transaction or automatic retry.
MemoryCacheStorage is exported from the package root and can serve bounded process-local caches, not just tests. Its defaults are 1,000 values, 16 MiB of UTF-8 key/value bytes, 1,000 tags, and 1,000 locks. Pass positive integer maxEntries, maxBytes, maxTags, and maxLocks options as the second constructor argument (after an optional clock). The byte limit is not a total JavaScript heap bound.
Capacity pressure removes expired entries before least-recently-read values. An oversized value is rejected without evicting other values. pruneExpired(budget = 100) performs a bounded, resumable sweep without installing a timer. Live locks are not evicted, and tag capacity exhaustion fails rather than resetting generations and reviving stale values.
cache.getStats() returns an immutable storage-instance observation of hits, misses, acknowledged writes, and available eviction/expiration counts. These are not namespace-scoped or global request metrics. Unobservable remote counts and unsupported custom-driver statistics are null, not fabricated zeroes.
Driver capabilities
| Driver | Distributed lock | Tag consistency |
|---|---|---|
| Bounded memory | No, process-local | Immediate |
| Redis | Yes, token-checked lease | Immediate |
| Cloudflare Workers KV | No, process-local | Eventual |
Each driver exposes client and capabilities. Redis uses SET NX PX, Lua release, atomic generation increments, and native millisecond expiry. Workers KV stores logical millisecond expiry while accepting its 60-second minimum physical cleanup.
Bind shared Next.js caching
@playstack/cache-next adapts a configured cache to Next's stable incremental cacheHandler contract and the Next 16 Cache Components cacheHandlers contract. It preserves structured-clone values and streamed Cache Component entries at the framework boundary rather than weakening the core exact-JSON cache.
The package also supplies a timestamped HMAC revalidation handler for both App Router and Pages Router applications. Route placement, cache policy, authorization, and deployment-specific Redis lifecycle remain application-owned.
Boundary
Cache is not authoritative application state, and provider consistency differences are not hidden. Storage/malformed-entry failures become retryable CacheUnavailableError. The application creates, observes, and closes the native client and chooses loaders that finish within the lock lease.
API entry points and requirements
Reference snapshot: @playstack/cache@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.
| Public entry point | Declaration file |
|---|---|
@playstack/cache | ./dist/index.d.ts |
@playstack/cache/errors | ./dist/errors.d.ts |
@playstack/cache/types | ./dist/types.d.ts |
@playstack/cache/testing | ./dist/testing.d.ts |
@playstack/cache/redis | ./dist/redis.d.ts |
@playstack/cache/cloudflare-kv | ./dist/cloudflare-kv.d.ts |
@playstack/cache/package.json | No TypeScript declaration (asset or metadata export). |
Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.
Peer dependencies
This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.
For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.