first commit
This commit is contained in:
80
docs/persistence/cache/cache-redis/README.md
vendored
Normal file
80
docs/persistence/cache/cache-redis/README.md
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# @suaveplan/cache-redis - AbstractCache backend over a minimal RedisLike interface — native SET ... EX, GET, DEL, EXISTS, TTL, SCAN. Reuses @suaveplan/cache-storage codecs (JSON / identity / base64).
|
||||
|
||||
`AbstractCache<T>` backend over a minimal `RedisLike` interface — native `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN`. Reuses `@suaveplan/cache-storage` codecs (JSON / identity / base64).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [errors](./src/errors/errors.md) — Defines the structured error surface for `@suaveplan/cache-redis`. Every failure inside the package — connection drops…
|
||||
- [errors](./src/errors/errors.md) — Defines the structured error surface for `@suaveplan/cache-redis`. Every failure inside the package — connection drops…
|
||||
- [redis-cache](./src/redis-cache/redis-cache.md) — `RedisCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and adapts a Redis-compatible store (via the minimal…
|
||||
- [redis-cache](./src/redis-cache/redis-cache.md) — `RedisCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and adapts a Redis-compatible store (via the minimal…
|
||||
- [redis-like](./src/redis-like/redis-like.md) — Declares the minimal Redis client surface `RedisCache` consumes. Any client conforming to this interface — `ioredis`, `…
|
||||
- [redis-like](./src/redis-like/redis-like.md) — Declares the minimal Redis client surface `RedisCache` consumes. Any client conforming to this interface — `ioredis`, `…
|
||||
- [registry-augment](./src/registry-augment/registry-augment.md) — Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `redis` entry, and side-effect-register…
|
||||
- [registry-augment](./src/registry-augment/registry-augment.md) — Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `redis` entry, and side-effect-register…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Summary
|
||||
|
||||
`@suaveplan/cache-redis` is the server-tier cache backend for the cache family. It plugs any Redis-compatible client (`ioredis`, `node-redis`, custom adapters over RESP3, KeyDB, Dragonfly) into the unified `AbstractCache<T>` surface. TTL is owned by the Redis server via native `SET ... EX`, so there are no client-side header bytes, no per-read expiry comparisons, and no client-side eviction tables — Redis's own `maxmemory-policy` decides what to evict. Side-effect import auto-registers `RedisCache` as the `"redis"` provider on `CacheFactory`, so `createCache("redis", { redis })` works after a single import.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/cache-redis
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { RedisCache } from "@suaveplan/cache-redis";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const cache = new RedisCache<{ id: string; email: string }>({
|
||||
redis: new Redis({ host: "redis.example.com" }),
|
||||
namespace: "users",
|
||||
defaultTtl: 5 * 60_000,
|
||||
});
|
||||
|
||||
await cache.set("alice", { id: "1", email: "alice@example.com" });
|
||||
const user = await cache.get("alice");
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [errors](./src/errors/errors.md) — `CACHE_REDIS_CODES`, `RedisCacheError`.
|
||||
- [redis-like](./src/redis-like/redis-like.md) — minimal `RedisLike` client surface.
|
||||
- [redis-cache](./src/redis-cache/redis-cache.md) — `RedisCache<T>` class.
|
||||
- [registry-augment](./src/registry-augment/registry-augment.md) — `CacheProviderRegistry` augmentation + auto-registration.
|
||||
|
||||
## Factory registration
|
||||
|
||||
```ts
|
||||
import "@suaveplan/cache-redis"; // side-effect: register provider
|
||||
import { createCache } from "@suaveplan/cache";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const cache = createCache("redis", { redis: new Redis() });
|
||||
```
|
||||
|
||||
## Why a separate package, not `cache-storage` over a future `storage-redis`?
|
||||
|
||||
Redis owns TTL natively (`EX`, `EXPIRE`) and eviction natively (`maxmemory-policy`). Layering the cache over a hypothetical `storage-redis` would:
|
||||
|
||||
1. Encode TTL into the value bytes (wasted — Redis ignores it).
|
||||
2. Require client-side eviction tracking (wasted — Redis evicts per policy).
|
||||
3. Lose `TTL key` introspection (Redis can tell us; the header can't).
|
||||
|
||||
So `RedisCache` talks to Redis directly via `RedisLike`, and `cache-storage` is reserved for genuinely byte-oriented stores (filesystem, S3, browser storage).
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Purpose |
|
||||
| --- | --- |
|
||||
| `@suaveplan/cache` | `AbstractCache<T>`, `CacheFactory`, `CacheProviderRegistry`, `CacheEvents`. |
|
||||
| `@suaveplan/cache-storage` | Shared `Codec<T>` contract + `JsonCodec` / `IdentityCodec`. |
|
||||
| `@suaveplan/error` | `defineCodes`, `SuaveplanError`. |
|
||||
| `@suaveplan/types` | `EventBusLike`. |
|
||||
| `tslib` | TypeScript helpers. |
|
||||
|
||||
76
docs/persistence/cache/cache-redis/src/errors/errors.md
vendored
Normal file
76
docs/persistence/cache/cache-redis/src/errors/errors.md
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the structured error surface for `@suaveplan/cache-redis`. Every failure inside the package — connection drops to the Redis server, individual command rejections, codec round-trip failures, oversized keys, invalid TTL arguments — surfaces as a `RedisCacheError` carrying one of the codes registered in `CACHE_REDIS_CODES`. Consumers programmatically branch on `error.code` rather than parsing free-form messages, and the optional `key` and `command` discriminants pinpoint exactly which command on which key triggered the failure.
|
||||
|
||||
## Features
|
||||
|
||||
- 5-code registry built with `defineCodes("CACHE_REDIS", [...])` so codes serialise identically across OpenTelemetry span events, the wire-format error envelope, and the i18n resolver.
|
||||
- `RedisCacheError` extends `SuaveplanError<CacheRedisCode>` for cause chains, structured `traceId`, and arbitrary `context`.
|
||||
- `key` and `command` discriminants on every error — present whenever a failure originates at a known key or command boundary.
|
||||
- Every code is exercised at a real call site by `errors.test.ts`, satisfying the §13 step 0 reachability gate.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CACHE_REDIS_CODES,
|
||||
RedisCacheError,
|
||||
} from "@suaveplan/cache-redis/errors";
|
||||
|
||||
try {
|
||||
await cache.set("k", value);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof RedisCacheError &&
|
||||
err.code === CACHE_REDIS_CODES.CONNECTION_FAILED
|
||||
) {
|
||||
metrics.increment("cache.redis.connection_failure");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CACHE_REDIS_CODES,
|
||||
RedisCacheError,
|
||||
} from "@suaveplan/cache-redis/errors";
|
||||
|
||||
function classify(err: unknown): "retry" | "fatal" | "ignore" {
|
||||
if (!(err instanceof RedisCacheError)) return "fatal";
|
||||
switch (err.code) {
|
||||
case CACHE_REDIS_CODES.CONNECTION_FAILED:
|
||||
case CACHE_REDIS_CODES.COMMAND_FAILED:
|
||||
return "retry";
|
||||
case CACHE_REDIS_CODES.CODEC_ERROR:
|
||||
return "ignore"; // cache miss is safe
|
||||
case CACHE_REDIS_CODES.KEY_TOO_LONG:
|
||||
case CACHE_REDIS_CODES.INVALID_TTL:
|
||||
return "fatal";
|
||||
default:
|
||||
return "fatal";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Kind | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `CACHE_REDIS_CODES` | const | Frozen object mapping reason → `"CACHE_REDIS.<REASON>"` value. |
|
||||
| `CacheRedisCode` | type | Union of the registered code values. |
|
||||
| `RedisCacheError` | class | Extends `SuaveplanError<CacheRedisCode>`. Constructor: `(code, messageKey, options?)` where options may carry `key`, `command`, `cause`, `context`, `traceId`. |
|
||||
| `RedisCacheErrorOptions` | type | `SuaveplanErrorOptions & { key?: string; command?: string }`. |
|
||||
|
||||
Registered codes: `CONNECTION_FAILED`, `COMMAND_FAILED`, `CODEC_ERROR`, `KEY_TOO_LONG`, `INVALID_TTL`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`CONNECTION_FAILED` is intentionally the catch-all for "the underlying `RedisLike` client threw something" — the cache cannot distinguish network errors from server-side `READONLY` rejects without parsing the cause, and most consumers treat both as "retry later." `COMMAND_FAILED` is reserved for failures that originate inside the cache itself — currently only `clear()` without a namespace; future commands that have explicit semantic preconditions will share this code.
|
||||
105
docs/persistence/cache/cache-redis/src/redis-cache/redis-cache.md
vendored
Normal file
105
docs/persistence/cache/cache-redis/src/redis-cache/redis-cache.md
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
# RedisCache
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`RedisCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and adapts a Redis-compatible store (via the minimal `RedisLike` interface) into the unified cache surface. The class uses native Redis primitives — `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN` — so TTL is owned by the Redis server (no client-side header bytes, no per-read expiry comparison) and the underlying `maxmemory-policy` handles eviction without duplicate client-side bookkeeping.
|
||||
|
||||
## Features
|
||||
|
||||
- Drop-in `AbstractCache<T>` subclass — inherits `wrap`, `getMultiple`, `setMultiple` from the base class with `AbortSignal` and stale-while-revalidate semantics unchanged.
|
||||
- Native TTL via `SET ... EX <seconds>` — milliseconds in the cache API are converted to seconds at the boundary, with sub-second TTLs rounded up to `EX: 1` so a positive TTL is never silently dropped.
|
||||
- Pluggable `Codec<T>` reused from `@suaveplan/cache-storage` — `JsonCodec<T>` default (UTF-8 string on the wire), `IdentityCodec` (base64-encoded binary).
|
||||
- Namespaced `clear()` via `SCAN`-then-`DEL` batches — bounded by namespace size, never `FLUSHDB`.
|
||||
- Refuses `clear()` without a namespace with `CACHE_REDIS.COMMAND_FAILED` — explicit foot-gun protection on shared Redis instances.
|
||||
- Client-side `getStats()` counters (`hits`, `misses`, `evictions`, `hitRate`). `size` returns `0` — Redis `DBSIZE` is whole-DB, not per-namespace, and `SCAN`-then-count is O(N) and inappropriate for a `getStats` call.
|
||||
- Structured `RedisCacheError` for every failure path with `key` and `command` discriminants.
|
||||
- Optional lifecycle events through any `EventBusLike<CacheEvents>`.
|
||||
- Auto-registered with the `CacheFactory` registry as the `"redis"` provider on side-effect import.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RedisCache } from "@suaveplan/cache-redis";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const cache = new RedisCache<{ id: string; email: string }>({
|
||||
redis: new Redis(),
|
||||
namespace: "users",
|
||||
defaultTtl: 5 * 60_000,
|
||||
});
|
||||
|
||||
await cache.set("alice", { id: "1", email: "alice@example.com" });
|
||||
const user = await cache.get("alice");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import "@suaveplan/cache-redis"; // side-effect: registers the "redis" provider
|
||||
import { createCache } from "@suaveplan/cache";
|
||||
import { IdentityCodec } from "@suaveplan/cache-storage";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const events = new EventBus();
|
||||
events.on("evict", ({ key }) => log.info("cache evicted", { key }));
|
||||
|
||||
const redis = new Redis({ host: "redis", maxRetriesPerRequest: 3 });
|
||||
|
||||
const binaryCache = createCache("redis" as never, {
|
||||
redis,
|
||||
namespace: "thumbnails",
|
||||
codec: new IdentityCodec(),
|
||||
events,
|
||||
} as never);
|
||||
|
||||
await binaryCache.set("avatar:42", await renderAvatar(42));
|
||||
|
||||
// Wrap, single-flight, SWR — inherited from AbstractCache.
|
||||
const summary = await binaryCache.wrap(
|
||||
`summary:${userId}`,
|
||||
() => buildSummary(userId),
|
||||
{ ttl: 60_000, staleWhileRevalidate: true, staleThreshold: 5_000 },
|
||||
);
|
||||
|
||||
// Clear only the "thumbnails" namespace — never touches the rest of the DB.
|
||||
await binaryCache.clear();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
`new RedisCache<T>(options: RedisCacheOptions<T>)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `redis` | `RedisLike` | — (required) | The underlying Redis client. |
|
||||
| `codec` | `Codec<T>` | `new JsonCodec<T>()` | Value codec. |
|
||||
| `defaultTtl` | `number` | `0` | TTL in ms when `set` omits one. `0` ⇒ no expiry. |
|
||||
| `events` | `EventBusLike` | `undefined` | Lifecycle event sink. |
|
||||
| `namespace` | `string` | `""` | Key prefix. Required for `clear()`. |
|
||||
|
||||
### Methods
|
||||
|
||||
Every `AbstractCacheShape<T>` method: `get`, `set`, `delete`, `clear`, `has`, `getStats`, `getMultiple`, `setMultiple`, `wrap`. See the parent `AbstractCache` documentation for inherited helpers.
|
||||
|
||||
### Thrown errors
|
||||
|
||||
- `RedisCacheError("CACHE_REDIS.CONNECTION_FAILED")` — the underlying client threw on a command.
|
||||
- `RedisCacheError("CACHE_REDIS.COMMAND_FAILED")` — `clear()` invoked without a namespace.
|
||||
- `RedisCacheError("CACHE_REDIS.CODEC_ERROR")` — codec encode or decode failure.
|
||||
- `RedisCacheError("CACHE_REDIS.KEY_TOO_LONG")` — key longer than 1024 chars.
|
||||
- `RedisCacheError("CACHE_REDIS.INVALID_TTL")` — negative TTL passed to `set`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cache uses base64 only when the codec is `IdentityCodec`; the `JsonCodec` default emits valid UTF-8 strings directly to avoid the 33% base64 overhead. Detection is via `instanceof IdentityCodec` — adding a different binary codec means setting a per-codec "is-binary-safe" capability flag (planned for a follow-up; the current check covers ≥95% of use).
|
||||
|
||||
`clear()` issues `SCAN 0 MATCH <ns>:* COUNT 1000` and `DEL`s each batch until the cursor returns `"0"`. The total deleted count is reflected in `getStats().evictions`. For very large namespaces this is O(N) — callers who need to wipe millions of keys should run the operation off the request path.
|
||||
|
||||
`hitCount` and `lastAccessedAt` tracked by `MemoryCache` are not maintained — Redis cannot return them in a single round-trip, and burning a second command on every read for advisory metrics would halve throughput. Use `TTL key` if you need to introspect remaining expiry.
|
||||
98
docs/persistence/cache/cache-redis/src/redis-like/redis-like.md
vendored
Normal file
98
docs/persistence/cache/cache-redis/src/redis-like/redis-like.md
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
# RedisLike
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Declares the minimal Redis client surface `RedisCache` consumes. Any client conforming to this interface — `ioredis`, `node-redis`, a custom adapter over the official RESP3 protocol, or even a non-Redis store like KeyDB or Dragonfly that exposes the same commands — is a valid argument to the cache. Constraining the surface to seven primitives (`set`, `get`, `del`, `exists`, `ttl`, `expire`, `scan`) plus optional `mget` keeps the contract small enough that an in-memory `FakeRedis` for tests is straightforward to write and audit.
|
||||
|
||||
## Features
|
||||
|
||||
- Seven required methods plus optional `mget` — chosen to match the exact set of commands `RedisCache` needs at runtime.
|
||||
- `set` accepts `{ EX, PX }` options matching the canonical Redis protocol; the cache only emits `EX`.
|
||||
- `scan` returns the cursor + matched keys as a single normalised shape (`RedisScanResult`), letting adapters bridge over both the `ioredis` tuple shape `[cursor, keys]` and the `node-redis` object shape `{ cursor, keys }`.
|
||||
- No transactional, pub/sub, or scripting primitives — those concerns belong to the application, not the cache.
|
||||
- Pure type module — zero runtime code, no `instanceof` requirement.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { RedisLike } from "@suaveplan/cache-redis/redis-like";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
// `ioredis` v5 conforms to RedisLike out of the box.
|
||||
const client: RedisLike = new Redis({ host: "redis.example.com" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
RedisLike,
|
||||
RedisScanResult,
|
||||
} from "@suaveplan/cache-redis/redis-like";
|
||||
import { createClient } from "redis";
|
||||
|
||||
// `node-redis` returns scan results in `{ cursor, keys }` shape and uses
|
||||
// `pExpire` for millisecond TTLs — a thin adapter normalises both.
|
||||
class NodeRedisAdapter implements RedisLike {
|
||||
readonly #client = createClient();
|
||||
async set(key: string, value: string, opts?: { EX?: number }) {
|
||||
return opts?.EX !== undefined
|
||||
? this.#client.set(key, value, { EX: opts.EX })
|
||||
: this.#client.set(key, value);
|
||||
}
|
||||
async get(k: string) {
|
||||
return this.#client.get(k);
|
||||
}
|
||||
async del(...keys: string[]) {
|
||||
return this.#client.del(keys);
|
||||
}
|
||||
async exists(...keys: string[]) {
|
||||
return this.#client.exists(keys);
|
||||
}
|
||||
async ttl(k: string) {
|
||||
return this.#client.ttl(k);
|
||||
}
|
||||
async expire(k: string, s: number) {
|
||||
const ok = await this.#client.expire(k, s);
|
||||
return ok ? 1 : 0;
|
||||
}
|
||||
async scan(
|
||||
cursor: string,
|
||||
opts?: { MATCH?: string; COUNT?: number },
|
||||
): Promise<RedisScanResult> {
|
||||
const result = await this.#client.scan(Number(cursor), {
|
||||
MATCH: opts?.MATCH,
|
||||
COUNT: opts?.COUNT,
|
||||
});
|
||||
return { cursor: String(result.cursor), keys: result.keys };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| --- | --- | --- |
|
||||
| `RedisLike` | interface | Required surface: `set`, `get`, `del`, `exists`, `ttl`, `expire`, `scan`, optional `mget`. |
|
||||
| `RedisSetOptions` | interface | `{ EX?: number; PX?: number }`. |
|
||||
| `RedisScanOptions` | interface | `{ MATCH?: string; COUNT?: number }`. |
|
||||
| `RedisScanResult` | interface | `{ cursor: string; keys: string[] }`. |
|
||||
|
||||
### Method semantics
|
||||
|
||||
- `set(key, value, opts?)` — `SET key value [EX seconds]`. Returns the server's response (typically `"OK"`); the cache ignores it.
|
||||
- `get(key)` — returns the value or `null` on miss.
|
||||
- `del(...keys)` — returns the number of keys actually deleted.
|
||||
- `exists(...keys)` — returns the number of matching keys.
|
||||
- `ttl(key)` — returns seconds, `-1` for "no expiry", `-2` for "missing key".
|
||||
- `expire(key, seconds)` — returns `1` on success, `0` on missing key.
|
||||
- `scan(cursor, opts?)` — returns `{ cursor, keys }` with `cursor === "0"` signalling end of iteration.
|
||||
- `mget(...keys)` — optional; if absent, `getMultiple` falls back to per-key `get`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The interface uses `string` for both keys and values because the canonical RESP protocol speaks strings; `RedisCache` runs UTF-8 / base64 conversion at the boundary. Adapters that expose `Buffer` (`ioredis` in `Buffer`-mode, `node-redis` with `returnBuffers`) work as long as the adapter normalises back to `string` before returning. A future binary-safe variant of the cache may add a parallel `RedisBinaryLike` contract; until then keeping `RedisLike` string-only keeps the implementation universal compatible.
|
||||
64
docs/persistence/cache/cache-redis/src/registry-augment/registry-augment.md
vendored
Normal file
64
docs/persistence/cache/cache-redis/src/registry-augment/registry-augment.md
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# registry-augment
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `redis` entry, and side-effect-registers `RedisCache` as the implementation behind that key on the shared `CacheFactory`. Importing this module — directly or transitively via the package root — is enough; the registration is idempotent and survives repeated imports or duplicate package versions resolving to the same singleton registry.
|
||||
|
||||
## Features
|
||||
|
||||
- TypeScript-side module augmentation makes `createCache("redis", config)` type-safe — the registry key becomes part of `CacheProviderType`, and the config parameter is inferred from `RedisCacheOptions<T>` via the registry slot.
|
||||
- Idempotent runtime registration — guarded by both an internal `registered` flag and a `hasCacheProvider("redis")` query against the live registry, so a double-import or a second package version pointing at the same registry never throws or overwrites.
|
||||
- Side-effect import — the `registry-augment.ts` module is listed in `"sideEffects"` in `package.json` so bundlers preserve the registration call when the rest of the package is tree-shaken.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Side-effect import: registers the "redis" provider.
|
||||
import "@suaveplan/cache-redis";
|
||||
|
||||
import { createCache } from "@suaveplan/cache";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const cache = createCache("redis", { redis: new Redis() });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Drive registration manually — useful in tests that need to control
|
||||
// provider availability ordering or confirm idempotency.
|
||||
import {
|
||||
hasCacheProvider,
|
||||
unregisterCacheProvider,
|
||||
} from "@suaveplan/cache";
|
||||
import { registerRedisProvider } from "@suaveplan/cache-redis";
|
||||
|
||||
unregisterCacheProvider("redis");
|
||||
registerRedisProvider();
|
||||
console.log(hasCacheProvider("redis")); // true
|
||||
registerRedisProvider(); // no-op
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Kind | Description |
|
||||
| --- | --- | --- |
|
||||
| `registerRedisProvider()` | function | Idempotently calls `registerCacheProvider("redis", RedisCache)`. Safe to invoke repeatedly. |
|
||||
|
||||
The module performs module augmentation:
|
||||
|
||||
```ts
|
||||
declare module "@suaveplan/cache" {
|
||||
interface CacheProviderRegistry {
|
||||
redis: typeof RedisCache;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `registered` module-local flag is set even when the registry already contained the provider (e.g. because another package version registered it first). That ensures the second branch is exercised at most once per module instance and that subsequent calls hit the fast path. The function is exported so test suites can drive it explicitly after a `clearCacheProviders()` call without re-importing the package.
|
||||
Reference in New Issue
Block a user