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-local-default

v0.1.2

Published

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

Readme

@dcsv-io/d2-caching-local-default

.NET mirror: DcsvIo.D2.Caching.Local.Default

Node/BFF authors who need a per-process ILocalCache inject this default in-process implementation — an LRU map store with TTL tracking, process-scope atomic primitives (set-if-absent, counters, locks), aggregate OTel counters, and @dcsv-io/d2-result shapes on every operation. This README covers usage, construction, per-operation semantics, TTL and eviction rules, telemetry, and disposal.

Install

pnpm add @dcsv-io/d2-caching-local-default

Usage

import { DefaultLocalCache } from "@dcsv-io/d2-caching-local-default";

using cache = new DefaultLocalCache({
  maxEntries: 10_000,
  keyPrefix: "bff:",
});

const setR = await cache.set("session:1", { userId: "u1" });
if (!setR.success) {
  throw new Error("set failed");
}

const getR = await cache.get<{ userId: string }>("session:1");
if (getR.success) {
  console.log(getR.data.userId);
}

const counter = await cache.increment("hits");
if (counter.success) {
  console.log(counter.data);
}

Construction + options

new DefaultLocalCache(
  options?: Partial<LocalCacheOptions>,
  clock?: () => number,
);

Partial<LocalCacheOptions> is merged over LOCAL_CACHE_DEFAULTS via createLocalCacheOptions (both owned by @dcsv-io/d2-caching-abstractions). The optional clock parameter defaults to Date.now and is the sole time source for value TTL, lock expiry, and remaining-TTL arithmetic — inject a fake in tests.

Construction throws RangeError on structurally broken numbers (negative / non-integer / non-finite maxEntries; non-finite defaultExpirationMs). Constructors throw; live operations return results. A throwing constructor is a composition-time bug, never per-call data flow.

Construct once at the composition root and share the instance (singleton-equivalent usage).

Public surface

  • DefaultLocalCache — implements ILocalCache (ICacheBasic + ICacheAtomic) and Disposable.
  • LOCAL_CACHE_METER_NAME, LOCAL_CACHE_METER_VERSION, LOCAL_CACHE_INSTRUMENTS — OpenTelemetry meter identity + instrument metadata SoT (used by counters and dual-runtime parity).

Basic: get, getMany, exists, getTtl, set, setMany, remove, removeMany (each accepts optional trailing signal?: AbortSignal; durations use *Ms number parameters).

Atomic: setNx, increment, acquireLock, releaseLock.

Result mapping

| Op | Success | Failure / special | | -- | ------- | ----------------- | | get | ok(value) | miss/expired → notFound; falsey key → VF(key) | | getMany | all hit → ok(map); partial → someFound (206) | none → notFound; falsey keys → VF(keys) | | exists | ok(true\|false) — never notFound | falsey key → VF(key) | | getTtl | live+TTL → ok(ms); live no TTL → ok(undefined) | absent/expired → notFound | | set / setMany | ok() | falsey / bad expirationMs → VF | | remove / removeMany | ok() (idempotent) | falsey → VF | | setNx | wrote → ok(true); exists → ok(false) with data === false | VF on bad input | | increment | ok(newValue) | non-counter existing → conflict (409); VF on bad input / overflow | | acquireLock | acquired → ok(true); held → ok(false) | VF on falsey key/lockId or non-positive finite expirationMs | | releaseLock | always ok() (idempotent) | VF on falsey key/lockId |

Validation failures name the TS parameter (expirationMs, amount, key, keys, entries, lockId) per the InputFailures call-site-name contract.

Validation (JS number guards)

Construction (RangeError, not D2Result):

  • maxEntries — non-negative safe integer (Number.isSafeInteger and >= 0); rejects NaN, ±Infinity, non-integers, and values outside safe-integer range.
  • defaultExpirationMs — finite number (Number.isFinite); Number.MAX_VALUE is accepted; <= 0 means “no default TTL” (not a construction error).

Per-call (validationFailed via InputFailures):

  • Falsey key / element of keys / map key in entries / lockId.
  • Optional expirationMs when present: must be finite and > 0 (NaN / ±Infinity / <= 0 → field expirationMs).
  • increment amount when present: must be a safe integer (NaN / ±Infinity / non-integer / non-safe → field amount).
  • increment after computing next = current + amount: if !Number.isSafeInteger(next) → field amount before write (store unchanged). Existing non-safe-integer / non-number values → conflict.
  • acquireLock expirationMs: finite and > 0 (required).

TTL semantics

  • Omitted expirationMs on write → defaultExpirationMs.
  • defaultExpirationMs <= 0 means entries store without expiry.
  • Explicit expirationMs must be a positive finite number (zero / negative / non-finite are validation failures, never "no TTL").
  • increment / setNx apply TTL only on create; existing keys preserve TTL (increment ignores expirationMs on the existing path).
  • getTtl has three outcomes: notFound / ok(ms) / ok(undefined).
  • Expiry is checked lazily on access with the injected clock (strict <=).

Capacity + eviction

maxEntries is a literal entry-count cap. Every access moves the touched entry to the most-recently-used position; when a write pushes the count past maxEntries, the least-recently-used entries are evicted synchronously in the same call, so the count never exceeds the cap. The .NET twin delegates to IMemoryCache (SizeLimit + 5% async compaction), where eviction lags the overflowing write; this implementation evicts deterministically instead. The evictions counter counts capacity evictions and expired-entries-detected-on-access only - explicit removes and overwrites are not evictions.

Values and expirations live in one record, so the .NET-documented transient no-TTL getTtl report after a capacity eviction cannot occur here.

Atomicity model

Atomic primitives are process-scope only (cluster coordination is IDistributedCache territory). Every operation body is synchronous (no internal awaits), which is what makes check-then-write windows atomic on the event loop. Locks live in a store separate from values; re-acquiring a held lock returns ok(false) even for the same lockId; release with a wrong id is a no-op.

The lock store is not capped by maxEntries and has no sweeper: an expired lock entry lingers until it is reacquired, released by its holder, or the cache is disposed (the .NET twin behaves the same way).

Telemetry

Meter "DcsvIo.D2.Caching.Local" v1.0.0 with five aggregate counters (no tags, no spans, no logs — per-call instrumentation would dominate sub-microsecond work):

| Name | Unit | Description | When | | ---- | ---- | ----------- | ---- | | d2.cache.local.hits | {hit} | Local cache hits. | get / getMany hits | | d2.cache.local.misses | {miss} | Local cache misses. | get / getMany misses | | d2.cache.local.sets | {write} | Local cache writes. | set / setMany / successful setNx / new-key increment | | d2.cache.local.removes | {removal} | Local cache removals (explicit). | every remove / removeMany key (incl. absent) | | d2.cache.local.evictions | {eviction} | Entries evicted by capacity / expiration. | capacity + expired-on-access only |

increment on an existing counter does not increment sets.

Counters bind to the global OpenTelemetry MeterProvider at construction time. Construct caches after telemetry setup (setupTelemetry from @dcsv-io/d2-telemetry) or the counters bind to the no-op meter for the life of the instance. With no MeterProvider registered every operation still works - metric adds are silent no-ops.

Disposal

dispose() clears all state and is idempotent; the class also implements Symbol.dispose for using declarations. Every operation on a disposed instance throws an Error ("DefaultLocalCache is disposed.") - a disposed cache is a lifecycle bug, not a per-call failure, so it does not surface as a D2Result. The disposal check runs before input validation.

Cancellation

signal parameters are accepted for interface parity and ignored: every operation completes synchronously in process, so there is no cancellation window (the .NET twin ignores CancellationToken the same way).

Divergences from the .NET implementation

  • Synchronous LRU vs async IMemoryCache compaction (see Capacity + eviction).
  • Single-record TTL (no transient no-TTL getTtl after capacity eviction).
  • Post-dispose exception type — every operation, including acquireLock / releaseLock, throws after dispose() (matching .NET fail-closed dispose on all public ops including locks). TS throws a plain Error with a pinned message; .NET throws ObjectDisposedException.
  • Validation additionally rejects non-finite expirationMs / non-safe-integer amount, and refuses an increment whose computed next value is outside Number.MAX_SAFE_INTEGER (field amount; write skipped). The number type admits values TimeSpan / long structurally cannot.
  • Counter values are safe-integer JS numbers (port-documented bound); precision beyond that range is not supported.
  • get<T> type parameters are caller-asserted and erased at runtime (the .NET generic cast throws on a wrong-type read; TS cannot).

Dependencies

  • @dcsv-io/d2-caching-abstractions — ports + options + InputFailures
  • @dcsv-io/d2-result — result factories
  • @dcsv-io/d2-utilitiesfalsey
  • @opentelemetry/api — counters

Sister packages

  • @dcsv-io/d2-caching-abstractions — ports + result mapping + key convention / PII guidance
  • @dcsv-io/d2-caching-distributed-redis — Redis + backplane
  • @dcsv-io/d2-caching-tiered — L1+L2 composition
  • .NET twin: DcsvIo.D2.Caching.Local.Default