{"_id":"@dcsv-io/d2-caching-tiered","_rev":"2-60f5214dda7de73c8fe83ef40242eef9","name":"@dcsv-io/d2-caching-tiered","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@dcsv-io/d2-caching-tiered","version":"0.1.1","_id":"@dcsv-io/d2-caching-tiered@0.1.1","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"dist":{"shasum":"4daf182028890cb8d2060d23a5cd3d26a1b8878e","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-tiered/-/d2-caching-tiered-0.1.1.tgz","fileCount":15,"integrity":"sha512-icbD2JzJyipZ27vio/GIS0ubiByHuR4M8orLI7nHp2GoCrtPfK8fpNS1qUq6kZS+Ei6d8umO89dRDWiIXwknPQ==","signatures":[{"sig":"MEQCIDG5Q/jorZQHr/n6XY4CvBCw+Iu9q/6s9x2o9r4o9NLJAiASUEh3w70fJUQ6TkA35RW7htKwNlSF8Y0BjcizXUf/Fg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":58949},"main":"./dist/index.js","type":"module","_from":"file:bundle/npm/dcsv-io-d2-caching-tiered-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","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"dcsv-tristan","email":"tristan@dcsv.io"},"_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-caching-tiered-0.1.1.tgz","_integrity":"sha512-icbD2JzJyipZ27vio/GIS0ubiByHuR4M8orLI7nHp2GoCrtPfK8fpNS1qUq6kZS+Ei6d8umO89dRDWiIXwknPQ==","_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-logging":"0.1.1","@dcsv-io/d2-caching-abstractions":"0.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.0.18","typescript":"5.9.3","testcontainers":"11.14.0","@vitest/coverage-v8":"4.0.18","@testcontainers/redis":"11.14.0","@dcsv-io/d2-caching-local-default":"0.1.1","@dcsv-io/d2-caching-distributed-redis":"0.1.1"},"_npmOperationalInternal":{"tmp":"tmp/d2-caching-tiered_0.1.1_1784262812464_0.12275160960780007","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dcsv-io/d2-caching-tiered","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-caching-abstractions":"0.1.2","@dcsv-io/d2-logging":"0.1.2","@dcsv-io/d2-result":"0.1.2"},"devDependencies":{"@testcontainers/redis":"11.14.0","@vitest/coverage-v8":"4.0.18","testcontainers":"11.14.0","typescript":"5.9.3","vitest":"4.0.18","@dcsv-io/d2-caching-distributed-redis":"0.1.2","@dcsv-io/d2-caching-local-default":"0.1.2"},"scripts":{"build":"tsc -b","test":"vitest run","test:coverage":"vitest run --coverage","test:integration":"vitest run --config vitest.integration.config.ts","type-check:test":"tsc -p tsconfig.test.json"},"_id":"@dcsv-io/d2-caching-tiered@0.1.2","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","_integrity":"sha512-EWW/MGxHN8ofKgfpw++ZU9OsT3bvca8owqO4dnCS2UV3MrMdb0eXIl1wW16rtblTqfGUxm8uEZ1YrIT8c5vH0Q==","_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-caching-tiered-0.1.2.tgz","_from":"file:bundle/npm/dcsv-io-d2-caching-tiered-0.1.2.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-EWW/MGxHN8ofKgfpw++ZU9OsT3bvca8owqO4dnCS2UV3MrMdb0eXIl1wW16rtblTqfGUxm8uEZ1YrIT8c5vH0Q==","shasum":"e2ed76846921f01abddec7b0808562b08897da88","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-tiered/-/d2-caching-tiered-0.1.2.tgz","fileCount":15,"unpackedSize":58874,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBjbmU0llutTDzt61eWZ9owf9zZmc94Eca7hJhg8PKCRAiBzPAC6U1sPFffkjvf4FvhWZuWFGBd8z/tDwyBlfg/BdQ=="}]},"_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-tiered_0.1.2_1784286838019_0.7765632785178529"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T04:33:32.296Z","modified":"2026-07-17T11:13:58.341Z","0.1.1":"2026-07-17T04:33:32.603Z","0.1.2":"2026-07-17T11:13:58.163Z"},"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-tiered\n\n.NET mirror: `DcsvIo.D2.Caching.Tiered`\n\nNode/BFF authors who need a composed L1+L2 `ITieredCache` inject this pure-composition implementation — reads check the in-process L1 first and fall through to the distributed L2, writes go L2-first so partial-write states are impossible, atomics route through L2 with L1 side-effects, and an optional invalidation backplane keeps every instance's L1 coherent under the universal everyone-acts rule. Every operation returns `@dcsv-io/d2-result` shapes.\n\n## Install\n\n```bash\npnpm add @dcsv-io/d2-caching-tiered\n```\n\n## Usage\n\n```ts\nimport { DefaultLocalCache } from \"@dcsv-io/d2-caching-local-default\";\nimport {\n  connectRedis,\n  createRedisCacheOptions,\n  JsonCacheSerializer,\n  RedisCacheInvalidationBackplane,\n  RedisDistributedCache,\n} from \"@dcsv-io/d2-caching-distributed-redis\";\nimport { DefaultTieredCache } from \"@dcsv-io/d2-caching-tiered\";\nimport type { ILogger } from \"@dcsv-io/d2-logging\";\n\nconst logger = /* host ILogger */ undefined as unknown as ILogger;\nconst l1 = new DefaultLocalCache({ keyPrefix: \"bff:\" });\nconst options = createRedisCacheOptions({\n  connectionString: process.env.REDIS_URL, // SECRET when credentials embedded — never log\n  keyPrefix: \"app:\",\n});\nconst redis = connectRedis(options);\nconst serializer = new JsonCacheSerializer();\nconst backplane = new RedisCacheInvalidationBackplane(redis, options, logger);\nawait backplane.ready; // channel SUBSCRIBE established (owned by redis package)\nconst l2 = new RedisDistributedCache({\n  redis,\n  options,\n  serializer,\n  logger,\n  // L2 backplane optional; tiered holds its own for *AndBroadcast*\n});\nconst cache = new DefaultTieredCache({ l1, l2, logger, backplane });\n\nawait cache.set(\"user:1\", { displayName: \"Ada\" });\nconst hit = await cache.get<{ displayName: string }>(\"user:1\");\nawait cache.setAndBroadcast(\"user:1\", { displayName: \"Ada Lovelace\" });\n\n// unsubscribes only; host still owns l1/l2/backplane/redis\nconst dispose = cache[Symbol.asyncDispose];\nawait dispose.call(cache);\n```\n\n## Construction\n\n```ts\nnew DefaultTieredCache({\n  l1: ILocalCache,\n  l2: IDistributedCache,\n  logger: ILogger,\n  backplane?: ICacheInvalidationBackplane,\n});\n```\n\nRequired `l1`, `l2`, and `logger`. Optional `backplane` enables `*AndBroadcast*` and cluster-wide L1 invalidation subscribe. Construction is synchronous. When the backplane is a Redis implementation, await that backplane's `ready` Promise before delivery-dependent work — this package does not own channel subscribe.\n\nThe tiered cache does not own or dispose `l1`, `l2`, or `backplane`. Disposing the tiered cache only unsubscribes its backplane handler. Construct once at the composition root and share the instance.\n\n## Public surface\n\n- **`DefaultTieredCache`** — implements `ITieredCache` (`ICacheBasic` + `ICacheAtomic` + `ICacheBroadcast`) and `AsyncDisposable`.\n- **`DefaultTieredCacheDeps`** — constructor dependency bag.\n- **`BACKPLANE_NOT_REGISTERED_MESSAGE`** — pinned registration-error text thrown by `*AndBroadcast*` when no backplane was passed.\n- **`TieredCacheOp`**, **`TIERED_ERROR_CODE_UNKNOWN`**, **`TieredCacheOpName`** — public twin-pin closed-set op names + errorCode sentinel for dual-runtime ContractFixtures parity.\n\n**Basic:** `get`, `getMany`, `exists`, `getTtl`, `set`, `setMany`, `remove`, `removeMany`.\n\n**Atomic:** `setNx`, `increment`, `acquireLock`, `releaseLock`.\n\n**Broadcast:** `setAndBroadcast`, `setManyAndBroadcast`, `removeAndBroadcast`, `removeManyAndBroadcast`.\n\nThis package does **not** implement `ICacheSet` (SADD/SCARD). Callers that need set primitives inject `IDistributedCache` directly.\n\n## Behavior\n\n**Reads** (`get` / `getMany` / `exists`):\n\n- Try L1 first.\n- On L1 miss, fall through to L2.\n- On L2 hit, populate L1 with the value (L1 default TTL; L2 remains the cluster freshness authority).\n- Return the result.\n- `exists`: L1 `true` short-circuits; otherwise query L2.\n- `getTtl`: L2 only (cluster source of truth).\n\n**Writes** (`set` / `setMany` / `remove` / `removeMany`):\n\n- L2 first — if L2 fails, return that failure and do not touch L1.\n- L1 second — only after L2 succeeded. The same `value` / `entries` / `expirationMs` / `signal` passed to L2 are passed to L1 on success.\n- If L1 fails after L2 succeeded, the L2-success result is returned. The L1 failure is logged at Warning and swallowed. L1 is the optional layer: an L1 failure on this instance must not fail a write the cluster accepted. The next read on this instance re-fetches from L2.\n\n**Atomic primitives** (`setNx` / `increment` / `acquireLock` / `releaseLock`):\n\n- Route through L2 (cluster source of truth) with full port arity (`value` / `amount` / `expirationMs` / `signal` as applicable).\n- After `setNx` succeeds: if L2 took the write, populate L1 with the same `value` and `expirationMs`; if the key already existed, drop L1.\n- After `increment` succeeds: always drop L1 (counters held in L1 would diverge).\n- Locks: pure L2 delegation — L1 is not involved. `releaseLock` is idempotent on ownership miss; a store-down failure surfaces as `serviceUnavailable` from L2.\n\n**Broadcast** (`setAndBroadcast` and siblings):\n\n- Perform the tiered write first; if the write fails, return that result and do **not** publish (all four ops).\n- On write success, publish via the injected `ICacheInvalidationBackplane`. Publish failure returns the backplane `D2Result` as-is.\n- All subscribers (every connected instance, including this one) receive the key and drop their L1 entry — everyone acts.\n- Missing backplane throws a plain `Error` with `BACKPLANE_NOT_REGISTERED_MESSAGE` (registration error, not a `D2Result`).\n\n## Backplane subscription\n\nWhen a backplane is passed at construction, the tiered cache subscribes in the constructor. Every received invalidation key triggers `remove` on L1. If that L1 remove fails, the failure is logged at Warning and processing continues; L1 stays stale on this instance until TTL or the next write/broadcast for that key. Disposing the tiered cache disposes the subscription only.\n\nWithout a backplane, plain ops still work; L1 caches drift across instances until their TTLs expire. `*AndBroadcast*` throws.\n\n## Design rationale: no ICacheSet\n\n`ITieredCache` deliberately does not implement `ICacheSet`. Set cardinality is cluster-only — there is no honest way to compose it across L1+L2 (an L1 set would only see this instance's adds; cluster cardinality lives in L2). Callers needing set primitives inject `IDistributedCache` directly.\n\n## Logging\n\nStructured Warning logs only (see also **Telemetry**):\n\n| When | Message | Bindings |\n| ---- | ------- | -------- |\n| L1 remove fails inside the invalidation handler | `Tiered cache L1 invalidation handler failed.` | `key`, `errorCode` |\n| L1 write/remove fails after L2 succeeded | `Tiered cache L1 write failed after L2 success.` | `operation`, `keyOrCount`, `errorCode` |\n\n`operation` is one of `set` / `setMany` / `remove` / `removeMany`. `errorCode` comes from the L1 `D2Result` (`\"unknown\"` when absent). Bindings never include exception messages. Keep PII out of cache keys (see abstractions).\n\n## Telemetry\n\nThis package publishes **no OTel meters**. L1 and L2 packages own their meters. Observability for this package is structured logging only — see **Logging**.\n\n## Result mapping\n\nPer-call validation and store failures surface from L1/L2 (`notFound`, `someFound`, `validationFailed` via `InputFailures`, `serviceUnavailable`, `conflict` on increment type mismatch). Tiered composition adds:\n\n| Path | Outcome |\n| ---- | ------- |\n| L2 write fails | Return L2 result; L1 untouched |\n| L1 fails after L2 write ok | Return L2 success; Warning log |\n| L2 hard fail on `getMany` missing keys (`!success && !isPartialSuccess`) | Propagate L2 even if L1 had partial hits |\n| L2 `someFound` on missing keys | Merge with L1 hits; `ok(merged)` or `someFound({ data: merged })` |\n| L2 `notFound` on missing keys | `someFound({ data: l1Hits })` or `notFound()` |\n| `*AndBroadcast*` write fails | Return write result; no publish |\n| `*AndBroadcast*` publish fails | Return backplane result |\n| `*AndBroadcast*` without backplane | Throws `Error` (`BACKPLANE_NOT_REGISTERED_MESSAGE`) |\n\n## Disposal\n\n`[Symbol.asyncDispose]` unsubscribes the backplane handler and is idempotent. It does not dispose L1, L2, or the backplane. Live operations remain callable after dispose (subscription already torn down).\n\n## Divergences from the .NET implementation\n\n- Construction uses a deps object instead of MS.DI `AddD2TieredCache()`; hosts compose L1/L2/logger/backplane explicitly.\n- Durations on L1/L2 use millisecond numbers (`expirationMs`), not `TimeSpan`.\n- Registration errors throw a plain `Error` with a pinned message, not `InvalidOperationException`.\n- When the backplane is Redis-backed, channel-subscribe readiness is owned by `@dcsv-io/d2-caching-distributed-redis` (`ready` Promise); this package keeps a sync constructor and a sync port `subscribe`.\n- Counter / lock numeric width follows the TS port (`number` within `Number.MAX_SAFE_INTEGER`).\n- Logging uses short structured redis-style Warning **message strings** + camelCase binding fields; dual-runtime parity is EventId **meanings** (L1 invalidation fail / L1 write fail after L2 ok), Warning-only, and `errorCode` presence — **not** LoggerMessage template byte-equality or .NET `\"SetAsync\"` / `\"SetManyAsync\"` operation-name strings (TS uses `\"set\"` / `\"setMany\"` / `\"remove\"` / `\"removeMany\"`).\n- Runtime dependencies are abstractions + result + logging only (no `@dcsv-io/d2-utilities`; .NET tiered likewise has no Utilities package dependency).\n\n## Dependencies\n\n| Package | Role |\n| ------- | ---- |\n| `@dcsv-io/d2-caching-abstractions` | Ports (`ITieredCache`, `ILocalCache`, `IDistributedCache`, `ICacheInvalidationBackplane`) |\n| `@dcsv-io/d2-result` | Result factories |\n| `@dcsv-io/d2-logging` | `ILogger` |\n\nNo backing-store packages at runtime — this package is pure composition. No `@dcsv-io/d2-utilities` (validation/falsey live in L1/L2). Typical hosts also depend on `@dcsv-io/d2-caching-local-default` and `@dcsv-io/d2-caching-distributed-redis` for L1/L2/backplane implementations.\n\n## Sister packages\n\n- `@dcsv-io/d2-caching-abstractions` — ports + result mapping\n- `@dcsv-io/d2-caching-local-default` — typical L1\n- `@dcsv-io/d2-caching-distributed-redis` — typical L2 + backplane\n- .NET twin: `DcsvIo.D2.Caching.Tiered`\n","readmeFilename":"README.md"}