@spfn/core 0.3.0-beta.4 → 0.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -4
- package/dist/authz/index.js +1 -381
- package/dist/authz/index.js.map +1 -1
- package/dist/db/index.d.ts +173 -27
- package/dist/db/index.js +192 -57
- package/dist/db/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js +1 -381
- package/dist/errors/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js +6 -387
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +18 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +61 -6
- package/dist/ops/index.js +330 -30
- package/dist/ops/index.js.map +1 -1
- package/dist/server/index.js +24 -1
- package/dist/server/index.js.map +1 -1
- package/docs/file-upload.md +195 -333
- package/package.json +6 -5
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +344 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +736 -0
- package/src/job/README.md +514 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.6",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -149,9 +149,9 @@
|
|
|
149
149
|
"test:client": "vitest run src/client",
|
|
150
150
|
"test:middleware": "vitest run src/middleware",
|
|
151
151
|
"test:env": "vitest run src/env",
|
|
152
|
-
"test:cache": "vitest run src/cache
|
|
153
|
-
"test:db": "vitest run src/db
|
|
154
|
-
"test:server": "vitest run src/server
|
|
152
|
+
"test:cache": "vitest run src/cache",
|
|
153
|
+
"test:db": "vitest run src/db",
|
|
154
|
+
"test:server": "vitest run src/server",
|
|
155
155
|
"check:circular": "madge --circular --extensions ts src/",
|
|
156
156
|
"type-check": "tsc --noEmit",
|
|
157
157
|
"publish:alpha": "node ../../scripts/publish-package.mjs alpha",
|
|
@@ -242,7 +242,8 @@
|
|
|
242
242
|
"dist",
|
|
243
243
|
"docs",
|
|
244
244
|
"README.md",
|
|
245
|
-
"LICENSE"
|
|
245
|
+
"LICENSE",
|
|
246
|
+
"src/**/README.md"
|
|
246
247
|
],
|
|
247
248
|
"publishConfig": {
|
|
248
249
|
"access": "public",
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# @spfn/core/cache — singleton Valkey/Redis connection manager (graceful-degrading)
|
|
2
|
+
|
|
3
|
+
Manages a process-global cache connection (ioredis `Redis | Cluster`) built from `CACHE_*`
|
|
4
|
+
env vars. `getCache()` / `getCacheRead()` hand you the **raw ioredis client** — there is no
|
|
5
|
+
typed wrapper. When no config is present, the library is missing, or the connection fails,
|
|
6
|
+
the module runs in **disabled mode** (getters return `undefined`) instead of throwing.
|
|
7
|
+
|
|
8
|
+
Valkey is a Redis fork (7.2.4 base) with 100% protocol compatibility, so `redis://` /
|
|
9
|
+
`rediss://` URLs and the ioredis client work unchanged against either.
|
|
10
|
+
|
|
11
|
+
## Import paths
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
getCache, getCacheRead, isCacheDisabled,
|
|
16
|
+
setCache, initCache, closeCache, getCacheInfo,
|
|
17
|
+
createCacheFromEnv, createSingleCacheFromEnv,
|
|
18
|
+
} from '@spfn/core/cache';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Single entry point — there is no `@spfn/core/cache/*` subpath. `ioredis` is an **optional
|
|
22
|
+
peer dependency** (`peerDependenciesMeta.ioredis.optional`); install it only when you want
|
|
23
|
+
cache enabled. It is `import()`-ed dynamically, so without it the bundle never references it.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Public API (complete)
|
|
28
|
+
|
|
29
|
+
Connection manager (`cache-manager.ts`):
|
|
30
|
+
|
|
31
|
+
| Export | Signature | Notes |
|
|
32
|
+
|--------|-----------|-------|
|
|
33
|
+
| `getCache()` | `() => Redis \| Cluster \| undefined` | Write instance. `undefined` if disabled/uninit. |
|
|
34
|
+
| `getCacheRead()` | `() => Redis \| Cluster \| undefined` | Read instance; falls back to write (`state.read ?? state.write`). |
|
|
35
|
+
| `isCacheDisabled()` | `() => boolean` | True when not configured / lib missing / connect failed. |
|
|
36
|
+
| `setCache(write, read?)` | `(Redis\|Cluster\|undefined, Redis\|Cluster\|undefined?) => void` | Manual/test injection. `read` defaults to `write`. Sets `disabled = !write`. |
|
|
37
|
+
| `initCache()` | `() => Promise<{ write?, read?, disabled: boolean }>` | `ping()`-tests then registers. Called by `startServer()`. |
|
|
38
|
+
| `closeCache()` | `() => Promise<void>` | `quit()`s connections, resets to disabled. Graceful-shutdown safe. |
|
|
39
|
+
| `getCacheInfo()` | `() => { hasWrite: boolean; hasRead: boolean; isReplica: boolean; disabled: boolean }` | Debug snapshot. |
|
|
40
|
+
|
|
41
|
+
Factory (`cache-factory.ts`):
|
|
42
|
+
|
|
43
|
+
| Export | Signature | Notes |
|
|
44
|
+
|--------|-----------|-------|
|
|
45
|
+
| `createCacheFromEnv()` | `() => Promise<CacheClients>` | Builds client(s) from env. Does **not** `ping` or register globally. |
|
|
46
|
+
| `createSingleCacheFromEnv()` | `() => Promise<Redis \| Cluster \| undefined>` | Returns only `write` from the above. |
|
|
47
|
+
|
|
48
|
+
Aliases & types:
|
|
49
|
+
|
|
50
|
+
- `createRedisFromEnv` = `createCacheFromEnv`, `createSingleRedisFromEnv` = `createSingleCacheFromEnv` (backward-compat).
|
|
51
|
+
- `type CacheClients = { write?: Redis \| Cluster; read?: Redis \| Cluster }`.
|
|
52
|
+
- `type RedisClients = CacheClients` (alias).
|
|
53
|
+
|
|
54
|
+
> **No such API.** There is **no** `cache` singleton object and **no** typed convenience
|
|
55
|
+
> wrapper. `cache.get<T>(...)`, `cache.set(key, value, { ttl })`, `cache.exists`,
|
|
56
|
+
> `cache.prefix(...)`, `cache.hset/hget/hgetall`, `cache.lpush/lrange` **as methods on a
|
|
57
|
+
> `cache` export do not exist** — older docs showing `import { cache } from '@spfn/core/cache'`
|
|
58
|
+
> are stale. You call those Redis commands directly on the ioredis instance returned by
|
|
59
|
+
> `getCache()` (e.g. `getCache()?.set(...)`, `getCache()?.hset(...)`). Likewise the env var is
|
|
60
|
+
> **`CACHE_URL`**, not `REDIS_URL` — `REDIS_URL` is ignored by the factory.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { startServer } from '@spfn/core/server';
|
|
68
|
+
|
|
69
|
+
await startServer(); // calls initCache() — cache enabled if CACHE_URL is set, else disabled
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { getCache, getCacheRead, isCacheDisabled } from '@spfn/core/cache';
|
|
74
|
+
|
|
75
|
+
// Write (raw ioredis client). Always null-check — may be undefined in disabled mode.
|
|
76
|
+
const cache = getCache();
|
|
77
|
+
if (cache)
|
|
78
|
+
{
|
|
79
|
+
await cache.set('user:123', JSON.stringify({ name: 'John' }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Read (replica if configured, else same as write)
|
|
83
|
+
const value = await getCacheRead()?.get('user:123');
|
|
84
|
+
const user = value ? JSON.parse(value) : null;
|
|
85
|
+
|
|
86
|
+
// Or branch on disabled mode
|
|
87
|
+
if (isCacheDisabled())
|
|
88
|
+
{
|
|
89
|
+
return fetchFromDatabase(); // alternative path
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Install ioredis to enable cache (optional):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm add ioredis # ioredis speaks both Valkey and Redis
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Environment variables
|
|
102
|
+
|
|
103
|
+
`createCacheFromEnv()` reads only `CACHE_*` vars, in this priority order:
|
|
104
|
+
|
|
105
|
+
| Pattern | Vars | Result |
|
|
106
|
+
|---------|------|--------|
|
|
107
|
+
| Single (most common) | `CACHE_URL` | one client used for both read & write |
|
|
108
|
+
| Master-Replica | `CACHE_WRITE_URL` + `CACHE_READ_URL` (both required) | separate write/read clients |
|
|
109
|
+
| Sentinel | `CACHE_SENTINEL_HOSTS` + `CACHE_MASTER_NAME` (+ `CACHE_PASSWORD`) | one client via sentinels |
|
|
110
|
+
| Cluster | `CACHE_CLUSTER_NODES` (+ `CACHE_PASSWORD`) | one `Cluster` client |
|
|
111
|
+
| TLS | `CACHE_TLS_REJECT_UNAUTHORIZED` | applied when URL is `rediss://` |
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Single
|
|
115
|
+
CACHE_URL=redis://localhost:6379
|
|
116
|
+
CACHE_URL=redis://:<password>@host:6379 # with auth (do not commit real secrets)
|
|
117
|
+
CACHE_URL=rediss://secure.host:6380 # TLS
|
|
118
|
+
CACHE_TLS_REJECT_UNAUTHORIZED=false # accept self-signed cert (rediss:// only)
|
|
119
|
+
|
|
120
|
+
# Master-Replica (BOTH must be set, else this pattern is skipped)
|
|
121
|
+
CACHE_WRITE_URL=redis://master:6379
|
|
122
|
+
CACHE_READ_URL=redis://replica:6379
|
|
123
|
+
|
|
124
|
+
# Sentinel
|
|
125
|
+
CACHE_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379
|
|
126
|
+
CACHE_MASTER_NAME=mymaster
|
|
127
|
+
|
|
128
|
+
# Cluster
|
|
129
|
+
CACHE_CLUSTER_NODES=node1:6379,node2:6379,node3:6379
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`hasCacheConfig()` checks `CACHE_URL`, `CACHE_WRITE_URL`, `CACHE_READ_URL`,
|
|
133
|
+
`CACHE_SENTINEL_HOSTS`, or `CACHE_CLUSTER_NODES`. If none are set, the factory short-circuits
|
|
134
|
+
to `{ write: undefined, read: undefined }` (disabled) **without importing ioredis**.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Lifecycle: factory vs manager
|
|
139
|
+
|
|
140
|
+
- **`createCacheFromEnv()`** is pure construction: it builds and returns ioredis client(s)
|
|
141
|
+
but never pings them and never touches global state. Use it for custom wiring/tests.
|
|
142
|
+
- **`initCache()`** is the managed path: it calls the factory, **`ping()`-tests** write (and
|
|
143
|
+
read, if distinct), and only then registers into the global singleton. On ping failure it
|
|
144
|
+
`quit()`s the connections and sets `disabled = true`. `startServer()` calls this for you.
|
|
145
|
+
- The singleton lives on `globalThis[Symbol.for('@spfn/core:cache')]`, so it survives the
|
|
146
|
+
CJS/ESM dual-package hazard (one shared state even if the module loads twice). All
|
|
147
|
+
consumers (`@spfn/auth`, your app, etc.) share the exact same connection.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { initCache, getCacheInfo, closeCache } from '@spfn/core/cache';
|
|
151
|
+
|
|
152
|
+
const { disabled } = await initCache(); // idempotent: returns existing state if already init'd
|
|
153
|
+
console.log(getCacheInfo()); // { hasWrite, hasRead, isReplica, disabled }
|
|
154
|
+
|
|
155
|
+
process.on('SIGTERM', async () =>
|
|
156
|
+
{
|
|
157
|
+
await closeCache();
|
|
158
|
+
process.exit(0);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`initCache()` is idempotent via `if (state.write) return …` — calling it again when already
|
|
163
|
+
connected returns the current state without re-pinging.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Using the client (raw ioredis)
|
|
168
|
+
|
|
169
|
+
`getCache()` returns a standard ioredis `Redis | Cluster`. All commands are the ioredis API,
|
|
170
|
+
not an SPFN wrapper — values are strings/buffers, **no automatic JSON serialization**, and
|
|
171
|
+
**TTL is the ioredis native form** (`'EX', seconds` or `'PX', ms`), not a `{ ttl }` option.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const cache = getCache();
|
|
175
|
+
if (cache)
|
|
176
|
+
{
|
|
177
|
+
// Strings + TTL (seconds via EX; milliseconds via PX)
|
|
178
|
+
await cache.set('session:abc', JSON.stringify(data), 'EX', 3600); // expires in 1h
|
|
179
|
+
await cache.set('flag', '1', 'PX', 500); // expires in 500ms
|
|
180
|
+
await cache.expire('session:abc', 60); // (re)set TTL, seconds
|
|
181
|
+
const ttl = await cache.ttl('session:abc'); // seconds remaining
|
|
182
|
+
|
|
183
|
+
// Existence / delete / counters
|
|
184
|
+
await cache.exists('session:abc'); // 1 | 0
|
|
185
|
+
await cache.del('session:abc');
|
|
186
|
+
await cache.incr('counter');
|
|
187
|
+
|
|
188
|
+
// Hashes
|
|
189
|
+
await cache.hset('user:123', 'name', 'John');
|
|
190
|
+
await cache.hget('user:123', 'name');
|
|
191
|
+
await cache.hgetall('user:123'); // Record<string,string>
|
|
192
|
+
|
|
193
|
+
// Lists
|
|
194
|
+
await cache.rpush('queue', 'item');
|
|
195
|
+
await cache.lrange('queue', 0, -1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Reads can target the replica
|
|
199
|
+
await getCacheRead()?.get('session:abc');
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
For full command coverage see the ioredis docs — anything ioredis supports works here.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Testing
|
|
207
|
+
|
|
208
|
+
`setCache()` injects instances (real or mock) without touching env or pinging:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { setCache, isCacheDisabled } from '@spfn/core/cache';
|
|
212
|
+
import { vi } from 'vitest';
|
|
213
|
+
|
|
214
|
+
beforeAll(() =>
|
|
215
|
+
{
|
|
216
|
+
setCache({ get: vi.fn(), set: vi.fn(), del: vi.fn(),
|
|
217
|
+
ping: vi.fn().mockResolvedValue('PONG'),
|
|
218
|
+
quit: vi.fn().mockResolvedValue('OK') } as any);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
afterAll(() => setCache(undefined)); // undefined → disabled mode
|
|
222
|
+
|
|
223
|
+
it('reports enabled', () => expect(isCacheDisabled()).toBe(false));
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`setCache(undefined)` flips the module to disabled mode (`disabled = !write`), which is the
|
|
227
|
+
simplest way to exercise your degradation path.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Pitfalls & anti-patterns
|
|
232
|
+
|
|
233
|
+
- **There is no `cache` object / typed wrapper.** Don't write
|
|
234
|
+
`import { cache } from '@spfn/core/cache'` or `cache.get<T>(k)` / `cache.set(k, v, { ttl })`
|
|
235
|
+
/ `cache.prefix(...)`. Use `getCache()` / `getCacheRead()` and call ioredis commands on the
|
|
236
|
+
returned client.
|
|
237
|
+
- **Always null-check the getter.** `getCache()` / `getCacheRead()` return `undefined` in
|
|
238
|
+
disabled mode. Use optional chaining (`getCache()?.set(...)`) or an `if (cache)` guard —
|
|
239
|
+
calling a method on `undefined` throws.
|
|
240
|
+
- **No automatic serialization.** ioredis stores strings/buffers. `JSON.stringify` on write
|
|
241
|
+
and `JSON.parse` on read yourself; passing an object stores `"[object Object]"`.
|
|
242
|
+
- **TTL is ioredis-native, not `{ ttl }`.** Use `set(key, val, 'EX', seconds)` /
|
|
243
|
+
`'PX', ms`, or a separate `expire(key, seconds)`. `EX` is seconds, `PX` is milliseconds —
|
|
244
|
+
don't mix the units up. There is no SPFN `{ ttl }` option object.
|
|
245
|
+
- **Env var is `CACHE_URL`, not `REDIS_URL`.** The factory only inspects `CACHE_*` keys;
|
|
246
|
+
`REDIS_URL` (and other `REDIS_*`) are ignored and leave you in disabled mode. (The
|
|
247
|
+
manager's JSDoc mentioning `VALKEY_*` / legacy `REDIS_*` is aspirational — the factory does
|
|
248
|
+
not read them.)
|
|
249
|
+
- **Master-Replica needs BOTH URLs.** Setting only `CACHE_WRITE_URL` (without
|
|
250
|
+
`CACHE_READ_URL`) skips the replica branch; if `CACHE_URL` is also unset you get disabled
|
|
251
|
+
mode. Set both, or just use `CACHE_URL`.
|
|
252
|
+
- **`createCacheFromEnv()` does not ping or register.** It returns possibly-unhealthy clients
|
|
253
|
+
and does not populate the global singleton. For the validated, shared instance use
|
|
254
|
+
`initCache()` (or `startServer()`); `getCache()` only sees instances registered via
|
|
255
|
+
`initCache()` / `setCache()`.
|
|
256
|
+
- **`closeCache()` is a no-op while disabled** and resets state to disabled afterward. After
|
|
257
|
+
closing, `getCache()` returns `undefined` until the next `initCache()` — don't cache the
|
|
258
|
+
client reference across a shutdown.
|
|
259
|
+
- **Secrets in URLs.** `CACHE_URL` / `CACHE_PASSWORD` carry credentials — keep them in
|
|
260
|
+
gitignored env files (`.env.server`), never commit real values. The factory masks the
|
|
261
|
+
password in its debug log (`:***@`); your own logging should do the same.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Complete example
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// cache-aside with the raw client + graceful degradation
|
|
269
|
+
import { getCache, getCacheRead } from '@spfn/core/cache';
|
|
270
|
+
|
|
271
|
+
async function getUser(id: string): Promise<User | null>
|
|
272
|
+
{
|
|
273
|
+
const cached = await getCacheRead()?.get(`user:${id}`);
|
|
274
|
+
if (cached)
|
|
275
|
+
{
|
|
276
|
+
return JSON.parse(cached) as User;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const user = await userRepo.findById(id);
|
|
280
|
+
if (user)
|
|
281
|
+
{
|
|
282
|
+
// 1h TTL; manual JSON serialization. No-op if cache disabled (optional chaining).
|
|
283
|
+
await getCache()?.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
|
|
284
|
+
}
|
|
285
|
+
return user;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function updateUser(id: string, data: Partial<User>): Promise<User>
|
|
289
|
+
{
|
|
290
|
+
const user = await userRepo.update(id, data);
|
|
291
|
+
await getCache()?.del(`user:${id}`); // invalidate
|
|
292
|
+
return user;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// manual wiring without env (e.g. custom config / one-off scripts)
|
|
298
|
+
import { setCache, getCache, closeCache } from '@spfn/core/cache';
|
|
299
|
+
import Redis from 'ioredis';
|
|
300
|
+
|
|
301
|
+
setCache(new Redis({ host: 'localhost', port: 6379, db: 0 }));
|
|
302
|
+
await getCache()?.set('k', 'v');
|
|
303
|
+
await closeCache();
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Types reference
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
interface CacheClients
|
|
312
|
+
{
|
|
313
|
+
write?: Redis | Cluster; // primary (also read if no replica)
|
|
314
|
+
read?: Redis | Cluster; // replica; getCacheRead() falls back to write
|
|
315
|
+
}
|
|
316
|
+
type RedisClients = CacheClients; // backward-compat alias
|
|
317
|
+
|
|
318
|
+
// getCacheInfo() return shape
|
|
319
|
+
{ hasWrite: boolean; hasRead: boolean; isReplica: boolean; disabled: boolean }
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`Redis` / `Cluster` are ioredis types (`import('ioredis')`).
|
|
323
|
+
|
|
324
|
+
## Related
|
|
325
|
+
|
|
326
|
+
- [@spfn/core/env](../env/README.md) — defining/validating `CACHE_*` vars
|
|
327
|
+
- [@spfn/core/logger](../logger/README.md) — the `@spfn/core:cache` child logger this module uses
|
|
328
|
+
- [@spfn/core](../../README.md) — main package documentation
|
|
329
|
+
- [ioredis](https://github.com/redis/ioredis) — full command/client API for the returned instance
|
|
330
|
+
- [Valkey](https://valkey.io/docs/) — protocol-compatible Redis fork
|