npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dcsv-io/d2-caching-abstractions

v0.1.2

Published

<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->

Readme

@dcsv-io/d2-caching-abstractions

.NET mirror: DcsvIo.D2.Caching.Abstractions

Node/BFF authors inject these cache ports without pulling Redis, logging, or DI wiring into domain-safe code. The package is the TypeScript twin of DcsvIo.D2.Caching.Abstractions: marker interfaces (ILocalCache / IDistributedCache / ITieredCache), fine-grained building blocks (ICacheBasic / ICacheAtomic / ICacheBroadcast / ICacheSet), plus ICacheInvalidationBackplane, ICacheSerializer, InputFailures, and LocalCacheOptions / LOCAL_CACHE_DEFAULTS. Every op returns Promise<D2Result<…>> / D2Result<…> via @dcsv-io/d2-result. No implementations ship here — only contracts and pure helpers.

Install

pnpm add @dcsv-io/d2-caching-abstractions

Public surface — building blocks

Fine-grained interfaces. Implementations declare which they support; marker interfaces compose them. Method names are camelCase (drop .NET Async suffix); cancellation is optional signal?: AbortSignal; durations are milliseconds (expirationMs, defaultExpirationMs, remaining TTL from getTtl).

  • ICacheBasicget / getMany / exists / getTtl / set / setMany / remove / removeMany
  • ICacheAtomicsetNx / increment / acquireLock / releaseLock
  • ICacheBroadcastsetAndBroadcast / setManyAndBroadcast / removeAndBroadcast / removeManyAndBroadcast
  • ICacheSetsetAdd / setCardinality / setRemove / setContains (cluster-only — Redis SADD/SCARD/SREM/SISMEMBER)

All operations return D2Result / D2Result<T> (async ops wrap in Promise). Multi-entry maps use ReadonlyMap<string, T> (callers may new Map(Object.entries(record))).

Falsey inputs (missing key, missing keys collection, missing entries map) return validationFailed with an InputError naming the offending parameter (built via InputFailures.required(...)). Implementations never throw for per-call caller mistakes — every per-call failure shape is observable on the result. Construction-time / DI-registration errors are a different lifecycle concern and do surface as throws — see the *AndBroadcast* carve-out below.

Cache key convention + PII

Cache keys follow the EntityName:{id} shape (Session:{userId}, Jwks:{kid}, WhoIs:{ip}, etc.) (EntityName:{id}). LocalCacheOptions.keyPrefix is a namespace prefix layered on top of that convention (handy when multiple caches share a process), not a substitute for it. Keep PII out of keys — keys leak into logs, traces, and store inspection. Hash any user-supplied identifier first.

Marker interfaces

What consumers inject. The marker name documents the cache scope at the dependency site so the reader sees the parameter and immediately knows the behavioral profile, without checking registration.

ILocalCache — composes ICacheBasic + ICacheAtomic. Per-process scope. Atomic ops at process scope. No broadcast (nothing outside this process can see local cache state). Use for instance-scoped data: per- instance fingerprint cache, per-instance counters, hot in-process lookups.

IDistributedCache — composes ICacheBasic + ICacheAtomic + ICacheBroadcast + ICacheSet. Cluster scope. Atomic ops cluster-wide. Every read hits the remote store (no L1). Use when freshness matters more than read speed: rate-limit counters, distributed locks, ephemeral session lookups.

ITieredCache — composes ICacheBasic + ICacheAtomic + ICacheBroadcast. Composed L1+L2. Reads check L1 first / fall through to L2 / populate L1. Writes go L2-first (L1 only if L2 succeeded). Atomic ops route through L2 with L1 invalidation as side effect. Use for read-heavy entity data where freshness within a few seconds is acceptable. Does NOT compose ICacheSet — set primitives are cluster-only and tiered composition would silently hide their cluster-wide nature. Callers needing SADD/SCARD inject IDistributedCache directly.

Shared blocks vs Set: Basic + Atomic + Broadcast are method-identical on IDistributedCache and ITieredCache. ICacheSet is only on IDistributedCache (not full structural identity). The marker distinction is behavioral — DI registration determines the concrete implementation; the parameter type at the call site tells the reader whether they consume "single-tier remote" or "two-tier composed."

Supporting types

LocalCacheOptions / LOCAL_CACHE_DEFAULTS / createLocalCacheOptionsmaxEntries (100_000), defaultExpirationMs (3_600_000 / 1h), keyPrefix (""). Factory merges an optional partial over defaults and returns a fresh mutable object. No POCO validation (mirrors .NET).

InputFailures — pre-built validationFailed factory (required(paramName) / required<T>(paramName)) used by impls so the cache surface stays errors-as-values rather than throws for per-call caller mistakes. Constructors / DI registration still throw — registration-time concern, not per-call input.

ICacheSerializer — pluggable serialization for distributed caches. contentType: string (free string, e.g. "application/json"); serialize / deserialize with Uint8Array. This package owns the port only; a default JSON implementation is part of the @dcsv-io/d2-caching-distributed-redis package surface. Local caches store objects directly and do not need this. Impls use COULD_NOT_BE_SERIALIZED / COULD_NOT_BE_DESERIALIZED failure codes.

ICacheInvalidationBackplane — optional pub/sub backplane for cross-instance L1 invalidation (AsyncDisposable). Tiered consumers subscribe at construction; *AndBroadcast* writes publish on every send. Provider-agnostic — swappable for Redis pub/sub, Postgres LISTEN/NOTIFY, in-process for tests.

Result mapping

| Op family | Success / partial | Failure / notes | | --- | --- | --- | | get | hit → ok(value) | miss → notFound; store down → serviceUnavailable | | getMany | all → ok(map); some → someFound(partial) | none → notFound | | exists | ok(true\|false) | store down → fail | | getTtl | remaining ms → ok(number); no expiry → ok(undefined) | absent → notFound | | set / setMany | ok | store down → fail | | remove / removeMany | ok (idempotent) | store down → fail | | setNx | wrote → ok(true); exists → ok(false) | store down → fail | | increment | ok(newValue); TTL applied only on key create; subsequent ops preserve TTL | type mismatch → conflict; store down → fail | | acquireLock | acquired → ok(true); held → ok(false) | requires expirationMs; store down → fail | | releaseLock | ok (idempotent — no-op if not held) | store down → fail | | *AndBroadcast* | same as plain counterparts | throws if no backplane registered (registration error, not D2Result) | | setAdd | new → ok(true); present → ok(false); TTL only on set create | store down → fail | | setCardinality | ok(count); absent → ok(0) | store down → fail | | setRemove | removed → ok(true); absent → ok(false) (idempotent) | store down → fail | | setContains | ok(true\|false) | store down → fail | | publishInvalidation / Many | ok | backplane error → fail | | bad input (falsey key, etc.) | — | validationFailed via InputFailures (impl duty; ports document) |

Counter width: number (not bigint) is the intentional TS ergonomic delta vs .NET long. Stay within Number.MAX_SAFE_INTEGER.

Broadcast variants — when to use

The *AndBroadcast* methods write/remove AND publish an invalidation message. Other instances' tiered caches subscribe to the backplane and drop their L1 copies on receipt — keeping cluster L1 caches in sync without polling.

Use the broadcast variant when:

  • The data is shared across instances (user profile, org settings, JWKS).
  • A user-initiated remove must be visible cluster-wide quickly.
  • Coordinated state changes that depend on every instance seeing the new state.

Use the plain (non-broadcast) variant when:

  • Cache-warming / startup seed.
  • Single-writer-single-reader keys (derived from this instance's process ID).
  • Hot-path writes where the broadcast cost dominates (counter ticks).
  • Refresh writes of effectively-the-same data.
  • Single-instance deployments (don't register a backplane).
  • Short-TTL data where staleness is naturally bounded.

Invalidation backplane

ICacheInvalidationBackplane powers *AndBroadcast* writes and any other "tell every instance to drop K from its L1" flow.

Everyone acts

Every subscriber receives every message — including messages this instance itself published. There is no sender-ID filter. The cost of self-receive is bounded (tiered next-read re-fetch from L2, or a no-op remove).

Dispose unsubscribes / stops further delivery

subscribe(handler) returns an AsyncDisposable. Disposing a subscription removes that handler from fan-out and stops further invalidation key delivery to it. The handler's AbortSignal is aborted on dispose (accompanies unsubscribe; does not replace it). One handler throw must not break delivery to other handlers. Each subscribe is independent. Dispose is idempotent. Disposing the backplane tears down shared provider resources, unsubscribes remaining handlers, and cancels in-flight handler work.

At-most-once delivery

Missed message → next read hits L2. Acceptable for cache invalidation.

const subscription = backplane.subscribe(async (key, signal) => {
  // Drop L1 entry for `key`. signal aborts when subscription is disposed.
  await dropLocal(key, signal);
});

// Later — dispose the subscription (AsyncDisposable):
const dispose = subscription[Symbol.asyncDispose];
await dispose.call(subscription);

Atomic on tiered — how it works

ITieredCache exposes the same atomic surface as IDistributedCache because the atomicity guarantee comes from L2 (the cluster source of truth). Pattern:

  • increment → L2 atomic increment; on success invalidate L1 + broadcast.
  • setNx → L2 SetNx; on success write L1 + broadcast; on fail invalidate L1.
  • acquireLock / releaseLock → pure delegation to L2 (lock state is coordination, not a cached value).

L1 is never authoritative for atomic state. L2 is. L1 just reflects (or invalidates). Concrete behavior lands in @dcsv-io/d2-caching-tiered.

Configuration carve-out

There is no shared DistributedCacheOptions in abstractions. Provider- specific options (Redis connection string, Sentinel topology, channel name for pub/sub, retry config, etc.) live on the implementation package's own options class. The few common fields (defaultExpirationMs, keyPrefix) are easier to redeclare per-impl than to inherit. Same for tiered: the tiered package declares its own options when there is a real knob to expose.

The invalidation channel constant (d2:cache:invalidations) lives on the Redis package options — abstractions own the interface only.

Usage

Inject markers at composition roots; this package registers nothing.

import type { ILocalCache, IDistributedCache, ITieredCache } from "@dcsv-io/d2-caching-abstractions";
import { InputFailures, createLocalCacheOptions } from "@dcsv-io/d2-caching-abstractions";

// Domain / handler code depends only on the marker:
async function loadProfile(cache: ITieredCache, userId: string) {
  return cache.get<Profile>(`Profile:${userId}`);
}

// Impls use InputFailures for per-call validation:
function guardKey(key: string) {
  if (!key) return InputFailures.required("key");
  return undefined;
}

const localOpts = createLocalCacheOptions({ keyPrefix: "jwks:" });

Telemetry

N/A in this package — abstractions are hook-free. Metrics live in local-default + redis implementations; tiered owns structured logs (not meters).

Dependencies

  • @dcsv-io/d2-result — every op returns D2Result / D2Result<T>
  • @dcsv-io/d2-i18n-keysTK.common.errors.NOT_NULL_VIOLATION for InputFailures

No runtime deps beyond those (no DI, no logging, no provider libs). This abstraction stays domain-safe so any handler can declare a cache dependency without dragging in implementation runtime.

Sister packages

  • @dcsv-io/d2-caching-local-default — local in-process impl
  • @dcsv-io/d2-caching-distributed-redis — Redis + backplane
  • @dcsv-io/d2-caching-tiered — L1+L2 composition
  • .NET twin: DcsvIo.D2.Caching.Abstractions