{"_id":"@dcsv-io/d2-caching-distributed-redis","_rev":"2-259bcc4fdba9ef61bef9dc1f75018e3e","name":"@dcsv-io/d2-caching-distributed-redis","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@dcsv-io/d2-caching-distributed-redis","version":"0.1.1","_id":"@dcsv-io/d2-caching-distributed-redis@0.1.1","maintainers":[{"name":"dcsv-tristan","email":"tristan@dcsv.io"}],"dist":{"shasum":"9c08aec9196fc9a4b558eb892db16ccc8188f8f5","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-distributed-redis/-/d2-caching-distributed-redis-0.1.1.tgz","fileCount":35,"integrity":"sha512-u24S3xf/TbqpeqeXFvKMEcuF6puZ3OGAHzwfb641/crMc1rZb0DfWLJt3Z5DUAf5535oqwzV8M+VwdlXEV/3bQ==","signatures":[{"sig":"MEUCIQD0aKC+46QVadw5Wl/AHReJzPpSU2mjlW/jYpUBDEDO7gIgBgZE4r9lvfkQr7zeigShFJ0vQEtxoN8v3L0gjaMf948=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":121212},"main":"./dist/index.js","type":"module","_from":"file:bundle/npm/dcsv-io-d2-caching-distributed-redis-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-distributed-redis-0.1.1.tgz","_integrity":"sha512-u24S3xf/TbqpeqeXFvKMEcuF6puZ3OGAHzwfb641/crMc1rZb0DfWLJt3Z5DUAf5535oqwzV8M+VwdlXEV/3bQ==","_npmVersion":"11.16.0","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","directories":{},"_nodeVersion":"24.18.0","dependencies":{"ioredis":"5.10.0","@dcsv-io/d2-result":"0.1.1","@opentelemetry/api":"1.9.0","@dcsv-io/d2-logging":"0.1.1","@dcsv-io/d2-i18n-keys":"0.1.1","@dcsv-io/d2-utilities":"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","@opentelemetry/sdk-metrics":"2.5.1"},"_npmOperationalInternal":{"tmp":"tmp/d2-caching-distributed-redis_0.1.1_1784262808954_0.8514131320377669","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dcsv-io/d2-caching-distributed-redis","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":{"@opentelemetry/api":"1.9.0","ioredis":"5.10.0","@dcsv-io/d2-caching-abstractions":"0.1.2","@dcsv-io/d2-i18n-keys":"0.1.2","@dcsv-io/d2-result":"0.1.2","@dcsv-io/d2-logging":"0.1.2","@dcsv-io/d2-utilities":"0.1.2"},"devDependencies":{"@opentelemetry/sdk-metrics":"2.5.1","@testcontainers/redis":"11.14.0","@vitest/coverage-v8":"4.0.18","testcontainers":"11.14.0","typescript":"5.9.3","vitest":"4.0.18"},"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-distributed-redis@0.1.2","description":"<!-- Copyright (c) DCSV. Licensed under the Apache License, Version 2.0. -->","_integrity":"sha512-fXtWppsHPFtn8e5nydHP+KKK2xIQooBFKu4qLQiFSPnAiqBZm/WhSbNLUNuxLVdtqORp19sRJ4zEia8agMOSmw==","_resolved":"/home/runner/work/D2-Public/D2-Public/bundle/npm/dcsv-io-d2-caching-distributed-redis-0.1.2.tgz","_from":"file:bundle/npm/dcsv-io-d2-caching-distributed-redis-0.1.2.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-fXtWppsHPFtn8e5nydHP+KKK2xIQooBFKu4qLQiFSPnAiqBZm/WhSbNLUNuxLVdtqORp19sRJ4zEia8agMOSmw==","shasum":"c7005cc28055ad3ba6a084d2e643e53912c8ab98","tarball":"https://registry.npmjs.org/@dcsv-io/d2-caching-distributed-redis/-/d2-caching-distributed-redis-0.1.2.tgz","fileCount":35,"unpackedSize":121344,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDGjkCSEo7fUBqOEIY0DXjLxF9emqLcdx41cO2CYW+8bwIgKyRPUi4lGroncbPZiaMgucb0aSPYAUHhJc3KxLJXXeM="}]},"_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-distributed-redis_0.1.2_1784286812595_0.3145191368021958"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T04:33:28.801Z","modified":"2026-07-17T11:13:32.896Z","0.1.1":"2026-07-17T04:33:29.094Z","0.1.2":"2026-07-17T11:13:32.737Z"},"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-distributed-redis\n\n.NET mirror: `DcsvIo.D2.Caching.Distributed.Redis`\n\nNode/BFF authors who need a cluster-scoped `IDistributedCache` inject this Redis implementation — Basic + Atomic + Broadcast + Set over Redis, a pub/sub invalidation backplane on channel `d2:cache:invalidations`, JSON serialization, aggregate OTel counters, and `@dcsv-io/d2-result` shapes on every operation.\n\n## Install\n\n```bash\npnpm add @dcsv-io/d2-caching-distributed-redis\n```\n\n## Usage\n\n```ts\nimport {\n  connectRedis,\n  createRedisCacheOptions,\n  JsonCacheSerializer,\n  RedisCacheInvalidationBackplane,\n  RedisDistributedCache,\n} from \"@dcsv-io/d2-caching-distributed-redis\";\nimport type { ILogger } from \"@dcsv-io/d2-logging\";\n\nconst options = createRedisCacheOptions({\n  connectionString: process.env.REDIS_URL, // SECRET when credentials embedded — never log\n  keyPrefix: \"app:\",\n});\nconst redis = connectRedis(options); // command client; host owns lifecycle\nconst logger = /* host ILogger */ undefined as unknown as ILogger;\nconst serializer = new JsonCacheSerializer();\nconst backplane = new RedisCacheInvalidationBackplane(redis, options, logger);\nawait backplane.ready; // channel SUBSCRIBE established (ioredis async setup; construction stays sync)\nconst cache = new RedisDistributedCache({\n  redis,\n  options,\n  serializer,\n  logger,\n  backplane,\n});\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// port subscribe(handler) is sync → AsyncDisposable — do not await subscribe itself\nawait using (backplane.subscribe(async (key) => {\n  // everyone-acts handler — key is as published (prefixed when from cache broadcast)\n})) {\n  // subscription active\n}\n// backplane dispose quits the owned subscriber only; host still owns `redis`\nconst dispose = backplane[Symbol.asyncDispose];\nawait dispose.call(backplane);\n```\n\n## Construction + options\n\n| Field | Default | Notes |\n| --- | --- | --- |\n| `connectionString` | (none) | Required for `connectRedis` only. SECRET when credentials embedded — never log. |\n| `defaultExpirationMs` | `3_600_000` | Applied when a write omits `expirationMs`. Values `<= 0` mean no default TTL. |\n| `keyPrefix` | `\"\"` | Prepended to store keys and broadcast payloads. |\n| `invalidationChannel` | `d2:cache:invalidations` | Shared with .NET. |\n| `commandTimeoutMs` | `2000` | Must be finite `> 0`. |\n| `connectTimeoutMs` | `5000` | Must be finite `> 0`. |\n| `connectRetries` | `3` | Non-negative safe integer. |\n| `abortOnConnectFail` | `false` | When false, ops return `serviceUnavailable` until Redis is up. |\n\n**Dual-connection:** host injects a **command** ioredis client; the backplane owns `commandRedis.duplicate()` as the subscriber. Publish uses the command client; channel subscribe uses the subscriber. After construct, **`await backplane.ready`** before delivery-dependent work. Dispose quits the **owned subscriber only**. `connectRedis` throws with fixed message `RedisCacheOptions.ConnectionString is required.` when the string is falsey — never interpolates input. Broken config throws at construction; live ops return results.\n\n## Public surface\n\n- `RedisDistributedCache` — full `IDistributedCache` (Basic + Atomic + Broadcast + Set)\n- `RedisDistributedCacheDeps` — constructor dependency bag type\n- `RedisCacheInvalidationBackplane` — `ready: Promise<void>`, sync port `subscribe(handler)`, publish, `AsyncDisposable`\n- `JsonCacheSerializer` — default `ICacheSerializer`\n- `REDIS_CACHE_METER_NAME`, `REDIS_CACHE_METER_VERSION`, `REDIS_CACHE_INSTRUMENTS`, `REDIS_CACHE_DEFAULTS`, `createRedisCacheOptions`, `RedisCacheOptions`\n- `INCREMENT_WITH_OPTIONAL_TTL`, `RELEASE_LOCK_IF_OWNER`, `SET_ADD_WITH_OPTIONAL_TTL` — public twin-pin Lua body constants (ContractFixtures parity; not an executor API)\n- `connectRedis` — builds a host-owned command client\n\n## Result mapping\n\n| Situation | Result |\n| --- | --- |\n| Miss | `notFound` |\n| Partial bulk hit | `someFound` (206) |\n| Redis / connection down | `serviceUnavailable` (including `releaseLock`) |\n| Increment WRONGTYPE / non-integer | `conflict` |\n| Increment amount not a safe integer / next not safe integer | validationFailed (`amount`) |\n| Serializer fail (get / set / setMany / setNx / broadcast wrappers) | whole-op `bubbleFail` |\n| `getMany` deserialize fail | **skips that entry** (no whole-op bubbleFail) |\n| Broadcast without backplane | **throws** registration `Error` |\n| `releaseLock` ownership miss | `ok` (idempotent) |\n| Aborted `AbortSignal` at entry | `canceled` |\n\n## Validation (JS number guards)\n\nConstruction (`RangeError` / fixed connect throw — not `D2Result`):\n\n- `commandTimeoutMs` / `connectTimeoutMs` — finite `> 0`.\n- `connectRetries` — non-negative safe integer.\n- `connectionString` falsey on `connectRedis` — fixed message throw (never\n interpolates input).\n\nPer-call (validationFailed via `InputFailures` unless noted):\n\n- Falsey `key` / keys / `lockId` / set member as applicable.\n- Optional `expirationMs` when present: finite and `> 0`.\n- `increment` `amount` when present: must be a safe integer\n (`NaN` / ±`Infinity` / `0.5` / non-safe → field `amount`, invalid not\n NOT_NULL).\n- `increment` result bound: Lua `INCREMENT_WITH_OPTIONAL_TTL` (byte-equal twin\n of .NET) refuses results outside ±9007199254740991 (`Number.MAX_SAFE_INTEGER`)\n by reversing `DECRBY` **in the same script** and returning\n `ERR safe_integer_overflow` → validationFailed field `amount`. No\n client-side race window; behavior matches .NET.\n- `acquireLock` `expirationMs`: finite and `> 0` (required param; invalid value\n → invalid field error).\n\n## TTL semantics\n\nDefault write TTL is 1 hour (`defaultExpirationMs`). Explicit `expirationMs` overrides when provided (must be finite `> 0`). `increment` and `setAdd` apply TTL **on create only** via Lua `PTTL < 0` gating. `getTtl`: absent → `notFound`; present no expiry → `ok(undefined)`; present with TTL → `ok(ms)`.\n\n## Atomics + Lua\n\nThree public twin-pin Lua script constants (not an executor API): INCRBY + safe-integer bound + optional PEXPIRE, compare-and-delete lock release, SADD + optional PEXPIRE. Bodies are byte-equivalent to .NET `RedisLuaScripts` and re-exported for dual-runtime ContractFixtures parity. Cluster-wide SET NX / INCR / setAdd atomicity is Redis-enforced.\n\n## Backplane\n\nEveryone acts (publisher receives own messages). At-most-once delivery. Multi-subscriber independence. Handler isolation (one throw does not break others). Dispose unsubscribes and quits the owned subscriber; publish after dispose does **not** throw. Default channel `d2:cache:invalidations` is shared with .NET. Construction is sync; channel readiness is `ready`. Port `subscribe(handler)` is **sync** (register handlers — never await the call itself).\n\n## Key prefix\n\nApplies to store keys **and** to keys published on `*AndBroadcast*` (prefixed payload).\n\n## Telemetry\n\n| Counter | Unit | Description |\n| --- | --- | --- |\n| `d2.cache.redis.hits` | `{hit}` | Redis cache hits. |\n| `d2.cache.redis.misses` | `{miss}` | Redis cache misses. |\n| `d2.cache.redis.sets` | `{write}` | Redis cache writes. |\n| `d2.cache.redis.removes` | `{removal}` | Redis cache removals. |\n| `d2.cache.redis.broadcasts` | `{broadcast}` | Invalidation messages published to backplane. |\n| `d2.cache.redis.errors` | `{error}` | Redis-side failures. |\n\nMeter name `DcsvIo.D2.Caching.Distributed.Redis` v`1.0.0` (`REDIS_CACHE_METER_NAME`). Construct the cache after host `setupTelemetry` so instruments bind to the real meter; without a MeterProvider counters are no-op-safe.\n\n## Logging\n\n`ILogger` is required. Redis-op failures log `{ operation, exceptionType, keyOrCount }`. Handler isolation logs `{ exceptionType, key }`. Never log exception messages or `connectionString`. Keep PII out of cache keys (see abstractions).\n\n## Cancellation\n\nAn aborted signal at method entry returns `canceled` without touching Redis. There is no mid-flight cancellation guarantee on every ioredis command.\n\n## Divergences from .NET\n\n- Client library is ioredis (not StackExchange.Redis).\n- `IConnectionMultiplexer` is not one ioredis TCP client — dual-connection (command + owned subscriber via `duplicate()`).\n- ioredis channel `subscribe` is Promise-based; .NET StackExchange registers channel Subscribe synchronously. TS keeps sync `new` and surfaces completion via `readonly ready: Promise<void>`.\n- Connection is injected (no MS.DI helper required beyond `connectRedis`).\n- Durations are millisecond numbers, not `TimeSpan`.\n- Counter / lock values are JS numbers within `Number.MAX_SAFE_INTEGER`;\n `increment` rejects non-safe-integer `amount` and non-safe-integer results\n (validationFailed field `amount`). Redis integer range is wider; callers must\n keep counters in the JS safe-integer band.\n- `AbortSignal` is entry-level only.\n- Broadcast registration and subscribe-after-dispose use plain `Error` (not BCL exception types).\n- Cache type has no dispose (host owns the command client), matching .NET not disposing the multiplexer from the cache.\n- Backplane dispose quits the owned subscriber (TS ownership of the duplicate).\n- JSON wire uses `JSON.stringify` / `JSON.parse` with camelCase + cycle ignore twin of STJ Web; residual deltas (prototype chain, `undefined` omission, `Date` encoding) may differ.\n\n## Dependencies\n\n| Package | Role |\n| --- | --- |\n| `@dcsv-io/d2-caching-abstractions` | Ports (`IDistributedCache`, backplane, serializer) |\n| `@dcsv-io/d2-result` | Result factories + `bubbleFail` + `fail` |\n| `@dcsv-io/d2-utilities` | `falsey` / helpers |\n| `@dcsv-io/d2-logging` | `ILogger` |\n| `@dcsv-io/d2-i18n-keys` | `TK.common.errors.COULD_NOT_BE_*` for serializer messages |\n| `@opentelemetry/api` | Meter / counters |\n| `ioredis` | Redis command + subscriber clients |\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-tiered` — L1+L2 composition\n- .NET twin: `DcsvIo.D2.Caching.Distributed.Redis`\n","readmeFilename":"README.md"}