Skip to content
 
 

Repository files navigation

DialCache

npm version Codecov OpenSSF Scorecard

DialCache is a TypeScript library that organizes caching into use cases, with runtime control and observability for each one.

  • Off by default: caching runs only inside an enable() scope.
  • Multi-layer: request-local → process-local → Redis.
  • Runtime policies per use case: layers, TTLs, and rollout ramps.
  • Targeted invalidation: one call per entity for its tracked Redis results.
  • Coalescing: same-key reads share work when a cache layer is active.
  • Fail-open: cache failures fall back to the loader.
  • Stale-on-error (opt-in): retained Redis values for selected source errors.
  • Shadow validation (opt-in): cache coherence checks through sampling.
  • Observability: Prometheus and Datadog metrics, including miss reasons.

Documentation · Getting started · API reference

Install

npm install dialcache

Requires Node.js 22.15.0 or newer within 22.x, or 23.8.0 and newer, for node:zlib zstd support. Use a supported LTS release in production. Install a Redis or Valkey client and a metrics client when using those integrations.

Usage

import { CacheLayer, DialCache, DialCacheKeyConfig } from "dialcache";

const dialcache = new DialCache();

// The loader: the database or service read.
async function fetchUser(userId: string) {
  console.log("Loading from source:", userId);
  return { id: userId, name: "Ada" };
}

// Register once; use getUser at read sites.
const getUser = dialcache.cached(fetchUser, {
  keyType: "user_id", // Entity kind; groups tracked results by id.
  useCase: "GetUser", // Operation name; part of the key and metric labels.
  cacheKey: (userId) => userId, // Include every input that changes the result.
  defaultConfig: new DialCacheKeyConfig({
    ttlSec: { [CacheLayer.LOCAL]: 60 },
  }),
});

// In a service, wrap each request's reads in one enable() call.
await dialcache.enable(async () => {
  await getUser("123"); // Loads from source and caches the result.
  await getUser("123"); // Reuses the value for up to 60 seconds.
});

await getUser("123"); // Outside enable(): loads from source again.

Results containing Date, bigint, or other non-JSON-compatible values need an explicit typed serializer, even when caching only in memory. Cached objects are shared references; copy before modifying.

getOrLoad() provides the same cache path for an inline loader and a direct key.

How reads work

Wrap each request's reads in one enable() call. Use disable() for nested mutation work; it restores pass-through behavior without evicting anything. Within an enabled call, the first active layer with a hit returns the value:

request-local → process-local → Redis / Valkey → loader
Layer Shared across Lifetime
Request-local Calls in one outer enable() scope Until that scope settles
Process-local Requests using one DialCache instance Insertion TTL, bounded by LRU capacity
Remote Instances sharing the Redis keyspace Physical TTL and logical age checks; optional watermarks

Local hits bypass Redis, including its invalidation checks. A Redis hit can warm an active local layer. Concurrent calls share work when a cache layer is active; set coalesce: false when callers need independent execution. How DialCache works covers the read path, publication rules, and freshness boundaries.

Runtime control

An operation's defaultConfig is its baseline. A cacheConfigProvider overrides individual policy fields for each enabled call. Ramps select stable cohorts of keys: raising a ramp adds keys, while lowering it removes keys without reshuffling the rest. A 10% key cohort need not account for 10% of traffic.

Policy changes apply to new invocations. They do not evict values or cancel work already admitted. Shadow validation has an independent ramp and compares sampled Redis values with the source in the background. It can also fill misses while remote serving is ramped down; callers do not wait for shadow checks or fills.

See Configuration and rollout for precedence and runtime examples, and Shadow validation for sampling and comparison behavior.

Documentation

Topic Guide
First reader and enabled scope Getting started
Result identity and invalidation groups Keys and identity
Defaults, overrides, and policy changes Configuration and rollout
Watermarks and mutation handling Targeted invalidation
Recovery from selected source failures Stale-on-error
Shared execution and deadlines Coalescing and liveness
Methods, options, and exports API reference
Go implementation and shared behavior contracts Go guide · Quint specification · Worked walkthrough

The Go port and the TypeScript library replay the same Quint-generated histories. Whether those histories reach every required boundary is decided by one language-neutral evaluator, node formal/witnesses.mjs evaluate, that any port runs over the same corpus; no port depends on another port's test suite for its completion evidence.

The documentation index also links to client setup, observability, upgrades, and the maintainer guide. Everything is readable as Markdown on GitHub.

MIT licensed. See LICENSE.

About

Fine-grained TypeScript caching with explicit local and Redis controls

Topics

Resources

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages