{"_id":"@dcsv-io/d2-caching-abstractions","_rev":"2-602787416951a87a90bd20840a6a1536","name":"@dcsv-io/d2-caching-abstractions","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@dcsv-io/d2-caching-abstractions","version":"0.1.1","_id":"@dcsv-io/d2-caching-abstractions@0.1.1","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"dist":{"shasum":"be5fc67327210391b8f46cce32922a6280db90ef","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-abstractions/-/d2-caching-abstractions-0.1.1.tgz","fileCount":51,"integrity":"sha512-lZkH77RTj1tQ1pb2y5VQgax8W0CGKkRzzVaWMI/O+DhWdSlXapYaNth4v0Xiv3tTo4/STiRTr+91mFKFfYfMvw==","signatures":[{"sig":"MEUCIEXuBikCUyuGnAOqy4wctMGno6z9u+kcrd1iT/hEuF+lAiEAjFUR15nO0MpkRkGjgEz1Y4Uy1SvKrmGizdYeEHZ0GH4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63997},"main":"./dist/index.js","type":"module","_from":"file:bundle/npm/dcsv-io-d2-caching-abstractions-0.1.1.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -b","test:coverage":"vitest run --coverage","type-check:test":"tsc -p tsconfig.test.json"},"_npmUser":{"name":"dcsv-tristan","email":"tristan@dcsv.io"},"_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-caching-abstractions-0.1.1.tgz","_integrity":"sha512-lZkH77RTj1tQ1pb2y5VQgax8W0CGKkRzzVaWMI/O+DhWdSlXapYaNth4v0Xiv3tTo4/STiRTr+91mFKFfYfMvw==","_npmVersion":"11.16.0","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","directories":{},"_nodeVersion":"24.18.0","dependencies":{"@dcsv-io/d2-result":"0.1.1","@dcsv-io/d2-i18n-keys":"0.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.0.18","typescript":"5.9.3","@vitest/coverage-v8":"4.0.18"},"_npmOperationalInternal":{"tmp":"tmp/d2-caching-abstractions_0.1.1_1784262807085_0.36244185122189454","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dcsv-io/d2-caching-abstractions","version":"0.1.2","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"dependencies":{"@dcsv-io/d2-i18n-keys":"0.1.2","@dcsv-io/d2-result":"0.1.2"},"devDependencies":{"@vitest/coverage-v8":"4.0.18","typescript":"5.9.3","vitest":"4.0.18"},"scripts":{"build":"tsc -b","test":"vitest run","test:coverage":"vitest run --coverage","type-check:test":"tsc -p tsconfig.test.json"},"_id":"@dcsv-io/d2-caching-abstractions@0.1.2","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","_integrity":"sha512-7K92LRjC3/MLvblDXlbYYnlGSCsgcnwuRUjz5K0vNi8VnpFwnAgxTCMIaToWgEV+wj2M++lcDhWMVvTJ36Z0wQ==","_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-caching-abstractions-0.1.2.tgz","_from":"file:bundle/npm/dcsv-io-d2-caching-abstractions-0.1.2.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-7K92LRjC3/MLvblDXlbYYnlGSCsgcnwuRUjz5K0vNi8VnpFwnAgxTCMIaToWgEV+wj2M++lcDhWMVvTJ36Z0wQ==","shasum":"ee67e46a83457836c23f7a39f1b78d88fedd6b1e","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-abstractions/-/d2-caching-abstractions-0.1.2.tgz","fileCount":51,"unpackedSize":64046,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDZj4AoHRbmym0oOU+2dPPRtSHgc5brm30oZ/dzbgaDSAIgRNl54PvMb5vHNUipcUV6Vw7Btia/R1Xyxjc6G85ejWI="}]},"_npmUser":{"name":"dcsv-tristan","email":"tristan@dcsv.io"},"directories":{},"maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/d2-caching-abstractions_0.1.2_1784286800312_0.900958412486788"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T04:33:26.865Z","modified":"2026-07-17T11:13:20.610Z","0.1.1":"2026-07-17T04:33:27.234Z","0.1.2":"2026-07-17T11:13:20.447Z"},"description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"readme":"<!--\nCopyright (c) DCSV. Licensed under the Apache License, Version 2.0.\n-->\n\n# @dcsv-io/d2-caching-abstractions\n\n.NET mirror: `DcsvIo.D2.Caching.Abstractions`\n\nNode/BFF authors inject these cache **ports** without pulling Redis, logging, or DI\nwiring into domain-safe code. The package is the TypeScript twin of\n`DcsvIo.D2.Caching.Abstractions`: marker interfaces (`ILocalCache` /\n`IDistributedCache` / `ITieredCache`), fine-grained building blocks\n(`ICacheBasic` / `ICacheAtomic` / `ICacheBroadcast` / `ICacheSet`), plus\n`ICacheInvalidationBackplane`, `ICacheSerializer`, `InputFailures`, and\n`LocalCacheOptions` / `LOCAL_CACHE_DEFAULTS`. Every op returns\n`Promise<D2Result<…>>` / `D2Result<…>` via `@dcsv-io/d2-result`. **No implementations**\nship here — only contracts and pure helpers.\n\n## Install\n\n```bash\npnpm add @dcsv-io/d2-caching-abstractions\n```\n\n## Public surface — building blocks\n\nFine-grained interfaces. Implementations declare which they support; marker\ninterfaces compose them. Method names are camelCase (drop .NET `Async` suffix);\ncancellation is optional `signal?: AbortSignal`; durations are milliseconds\n(`expirationMs`, `defaultExpirationMs`, remaining TTL from `getTtl`).\n\n- **`ICacheBasic`** — `get` / `getMany` / `exists` / `getTtl` / `set` /\n `setMany` / `remove` / `removeMany`\n- **`ICacheAtomic`** — `setNx` / `increment` / `acquireLock` / `releaseLock`\n- **`ICacheBroadcast`** — `setAndBroadcast` / `setManyAndBroadcast` /\n `removeAndBroadcast` / `removeManyAndBroadcast`\n- **`ICacheSet`** — `setAdd` / `setCardinality` / `setRemove` / `setContains`\n (cluster-only — Redis SADD/SCARD/SREM/SISMEMBER)\n\nAll operations return `D2Result` / `D2Result<T>` (async ops wrap in\n`Promise`). Multi-entry maps use `ReadonlyMap<string, T>` (callers may\n`new Map(Object.entries(record))`).\n\nFalsey inputs (missing key, missing keys collection, missing entries map)\nreturn `validationFailed` with an `InputError` naming the offending parameter\n(built via `InputFailures.required(...)`). Implementations never throw for\n**per-call** caller mistakes — every per-call failure shape is observable on\nthe result. Construction-time / DI-registration errors are a different\nlifecycle concern and **do** surface as throws — see the `*AndBroadcast*`\ncarve-out below.\n\n## Cache key convention + PII\n\nCache keys follow the `EntityName:{id}` shape (`Session:{userId}`,\n`Jwks:{kid}`, `WhoIs:{ip}`, etc.) (`EntityName:{id}`).\n`LocalCacheOptions.keyPrefix` is a **namespace** prefix layered on top of that\nconvention (handy when multiple caches share a process), not a substitute for\nit. **Keep PII out of keys** — keys leak into logs, traces, and store\ninspection. Hash any user-supplied identifier first.\n\n## Marker interfaces\n\nWhat consumers inject. The marker name documents the cache scope at the\ndependency site so the reader sees the parameter and immediately knows the\nbehavioral profile, without checking registration.\n\n**`ILocalCache`** — composes `ICacheBasic` + `ICacheAtomic`.\nPer-process scope. Atomic ops at process scope. No broadcast (nothing outside\nthis process can see local cache state). Use for instance-scoped data: per-\ninstance fingerprint cache, per-instance counters, hot in-process lookups.\n\n**`IDistributedCache`** — composes `ICacheBasic` + `ICacheAtomic` +\n`ICacheBroadcast` + `ICacheSet`.\nCluster scope. Atomic ops cluster-wide. Every read hits the remote store\n(no L1). Use when freshness matters more than read speed: rate-limit counters,\ndistributed locks, ephemeral session lookups.\n\n**`ITieredCache`** — composes `ICacheBasic` + `ICacheAtomic` +\n`ICacheBroadcast`.\nComposed L1+L2. Reads check L1 first / fall through to L2 / populate L1.\nWrites go L2-first (L1 only if L2 succeeded). Atomic ops route through L2 with\nL1 invalidation as side effect. Use for read-heavy entity data where freshness\nwithin a few seconds is acceptable. **Does NOT compose `ICacheSet`** — set\nprimitives are cluster-only and tiered composition would silently hide their\ncluster-wide nature. Callers needing SADD/SCARD inject `IDistributedCache`\ndirectly.\n\n**Shared blocks vs Set:** Basic + Atomic + Broadcast are method-identical on\n`IDistributedCache` and `ITieredCache`. **`ICacheSet` is only on\n`IDistributedCache`** (not full structural identity). The marker distinction\nis behavioral — DI registration determines the concrete implementation; the\nparameter type at the call site tells the reader whether they consume\n\"single-tier remote\" or \"two-tier composed.\"\n\n## Supporting types\n\n**`LocalCacheOptions`** / **`LOCAL_CACHE_DEFAULTS`** / **`createLocalCacheOptions`**\n— `maxEntries` (100_000), `defaultExpirationMs` (3_600_000 / 1h),\n`keyPrefix` (`\"\"`). Factory merges an optional partial over defaults and\nreturns a fresh mutable object. No POCO validation (mirrors .NET).\n\n**`InputFailures`** — pre-built `validationFailed` factory\n(`required(paramName)` / `required<T>(paramName)`) used by impls so the\ncache surface stays errors-as-values rather than throws for per-call caller\nmistakes. Constructors / DI registration still throw — registration-time\nconcern, not per-call input.\n\n**`ICacheSerializer`** — pluggable serialization for distributed caches.\n`contentType: string` (free string, e.g. `\"application/json\"`);\n`serialize` / `deserialize` with `Uint8Array`. This package owns the port only; a default JSON implementation is part of the\n`@dcsv-io/d2-caching-distributed-redis` package surface. Local\ncaches store objects directly and do not need this. Impls use `COULD_NOT_BE_SERIALIZED` /\n`COULD_NOT_BE_DESERIALIZED` failure codes.\n\n**`ICacheInvalidationBackplane`** — optional pub/sub backplane for\ncross-instance L1 invalidation (`AsyncDisposable`). Tiered consumers\nsubscribe at construction; `*AndBroadcast*` writes publish on every send.\nProvider-agnostic — swappable for Redis pub/sub, Postgres LISTEN/NOTIFY,\nin-process for tests.\n\n## Result mapping\n\n| Op family | Success / partial | Failure / notes |\n| --- | --- | --- |\n| `get` | hit → `ok(value)` | miss → `notFound`; store down → `serviceUnavailable` |\n| `getMany` | all → `ok(map)`; some → `someFound(partial)` | none → `notFound` |\n| `exists` | `ok(true\\|false)` | store down → fail |\n| `getTtl` | remaining ms → `ok(number)`; no expiry → `ok(undefined)` | absent → `notFound` |\n| `set` / `setMany` | `ok` | store down → fail |\n| `remove` / `removeMany` | `ok` (**idempotent**) | store down → fail |\n| `setNx` | wrote → `ok(true)`; exists → `ok(false)` | store down → fail |\n| `increment` | `ok(newValue)`; **TTL applied only on key create; subsequent ops preserve TTL** | type mismatch → `conflict`; store down → fail |\n| `acquireLock` | acquired → `ok(true)`; held → `ok(false)` | **requires** `expirationMs`; store down → fail |\n| `releaseLock` | `ok` (**idempotent** — no-op if not held) | store down → fail |\n| `*AndBroadcast*` | same as plain counterparts | **throws** if no backplane registered (registration error, not D2Result) |\n| `setAdd` | new → `ok(true)`; present → `ok(false)`; **TTL only on set create** | store down → fail |\n| `setCardinality` | `ok(count)`; absent → `ok(0)` | store down → fail |\n| `setRemove` | removed → `ok(true)`; absent → `ok(false)` (**idempotent**) | store down → fail |\n| `setContains` | `ok(true\\|false)` | store down → fail |\n| `publishInvalidation` / `Many` | `ok` | backplane error → fail |\n| bad input (falsey key, etc.) | — | `validationFailed` via `InputFailures` (impl duty; ports document) |\n\n**Counter width:** `number` (not `bigint`) is the intentional TS ergonomic\ndelta vs .NET `long`. Stay within `Number.MAX_SAFE_INTEGER`.\n\n## Broadcast variants — when to use\n\nThe `*AndBroadcast*` methods write/remove AND publish an invalidation message.\nOther instances' tiered caches subscribe to the backplane and drop their L1\ncopies on receipt — keeping cluster L1 caches in sync without polling.\n\n**Use the broadcast variant when:**\n\n- The data is shared across instances (user profile, org settings, JWKS).\n- A user-initiated remove must be visible cluster-wide quickly.\n- Coordinated state changes that depend on every instance seeing the new state.\n\n**Use the plain (non-broadcast) variant when:**\n\n- Cache-warming / startup seed.\n- Single-writer-single-reader keys (derived from this instance's process ID).\n- Hot-path writes where the broadcast cost dominates (counter ticks).\n- Refresh writes of effectively-the-same data.\n- Single-instance deployments (don't register a backplane).\n- Short-TTL data where staleness is naturally bounded.\n\n## Invalidation backplane\n\n`ICacheInvalidationBackplane` powers `*AndBroadcast*` writes and any other\n\"tell every instance to drop K from its L1\" flow.\n\n### Everyone acts\n\nEvery subscriber receives every message — **including messages this instance\nitself published**. There is no sender-ID filter. The cost of self-receive is\nbounded (tiered next-read re-fetch from L2, or a no-op remove).\n\n### Dispose unsubscribes / stops further delivery\n\n`subscribe(handler)` returns an `AsyncDisposable`. Disposing a subscription\n**removes that handler from fan-out** and **stops further invalidation key\ndelivery** to it. The handler's `AbortSignal` is aborted on dispose\n(accompanies unsubscribe; does not replace it). One handler throw must not\nbreak delivery to other handlers. Each `subscribe` is independent. Dispose is\nidempotent. Disposing the backplane tears down shared provider resources,\nunsubscribes remaining handlers, and cancels in-flight handler work.\n\n### At-most-once delivery\n\nMissed message → next read hits L2. Acceptable for cache invalidation.\n\n```ts\nconst subscription = backplane.subscribe(async (key, signal) => {\n  // Drop L1 entry for `key`. signal aborts when subscription is disposed.\n  await dropLocal(key, signal);\n});\n\n// Later — dispose the subscription (AsyncDisposable):\nconst dispose = subscription[Symbol.asyncDispose];\nawait dispose.call(subscription);\n```\n\n## Atomic on tiered — how it works\n\n`ITieredCache` exposes the same atomic surface as `IDistributedCache` because\nthe atomicity guarantee comes from L2 (the cluster source of truth). Pattern:\n\n- **`increment`** → L2 atomic increment; on success invalidate L1 + broadcast.\n- **`setNx`** → L2 SetNx; on success write L1 + broadcast; on fail invalidate L1.\n- **`acquireLock` / `releaseLock`** → pure delegation to L2 (lock state is\n coordination, not a cached value).\n\nL1 is never authoritative for atomic state. L2 is. L1 just reflects (or\ninvalidates). Concrete behavior lands in `@dcsv-io/d2-caching-tiered`.\n\n## Configuration carve-out\n\nThere is **no** shared `DistributedCacheOptions` in abstractions. Provider-\nspecific options (Redis connection string, Sentinel topology, channel name\nfor pub/sub, retry config, etc.) live on the implementation package's own\noptions class. The few common fields (`defaultExpirationMs`, `keyPrefix`) are\neasier to redeclare per-impl than to inherit. Same for tiered: the tiered\npackage declares its own options when there is a real knob to expose.\n\nThe invalidation channel constant (`d2:cache:invalidations`) lives on the\nRedis package options — abstractions own the **interface only**.\n\n## Usage\n\nInject markers at composition roots; this package registers nothing.\n\n```ts\nimport type { ILocalCache, IDistributedCache, ITieredCache } from \"@dcsv-io/d2-caching-abstractions\";\nimport { InputFailures, createLocalCacheOptions } from \"@dcsv-io/d2-caching-abstractions\";\n\n// Domain / handler code depends only on the marker:\nasync function loadProfile(cache: ITieredCache, userId: string) {\n  return cache.get<Profile>(`Profile:${userId}`);\n}\n\n// Impls use InputFailures for per-call validation:\nfunction guardKey(key: string) {\n  if (!key) return InputFailures.required(\"key\");\n  return undefined;\n}\n\nconst localOpts = createLocalCacheOptions({ keyPrefix: \"jwks:\" });\n```\n\n## Telemetry\n\n**N/A in this package** — abstractions are hook-free. Metrics live in\n**local-default + redis** implementations; **tiered owns structured logs\n(not meters)**.\n\n## Dependencies\n\n- `@dcsv-io/d2-result` — every op returns `D2Result` / `D2Result<T>`\n- `@dcsv-io/d2-i18n-keys` — `TK.common.errors.NOT_NULL_VIOLATION` for `InputFailures`\n\nNo runtime deps beyond those (no DI, no logging, no provider libs). This\nabstraction stays domain-safe so any handler can declare a cache dependency\nwithout dragging in implementation runtime.\n\n## Sister packages\n\n- `@dcsv-io/d2-caching-local-default` — local in-process impl\n- `@dcsv-io/d2-caching-distributed-redis` — Redis + backplane\n- `@dcsv-io/d2-caching-tiered` — L1+L2 composition\n- .NET twin: `DcsvIo.D2.Caching.Abstractions`\n","readmeFilename":"README.md"}