ioredis-toolkit 0.0.9 → 0.5.0
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/CHANGELOG.md +67 -0
- package/LICENSE +1 -1
- package/README.md +68 -1058
- package/dist/cache/cache.d.ts +30 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +59 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/config.d.ts +12 -0
- package/dist/cache/config.d.ts.map +1 -0
- package/dist/cache/config.js +13 -0
- package/dist/cache/config.js.map +1 -0
- package/dist/cache/types.d.ts +32 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +5 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/index.d.ts +43 -51
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -44
- package/dist/index.js.map +1 -0
- package/dist/lock/config.d.ts +12 -0
- package/dist/lock/config.d.ts.map +1 -0
- package/dist/lock/config.js +8 -0
- package/dist/lock/config.js.map +1 -0
- package/dist/lock/lock.d.ts +20 -0
- package/dist/lock/lock.d.ts.map +1 -0
- package/dist/lock/lock.js +44 -0
- package/dist/lock/lock.js.map +1 -0
- package/dist/lock/types.d.ts +19 -0
- package/dist/lock/types.d.ts.map +1 -0
- package/dist/lock/types.js +2 -0
- package/dist/lock/types.js.map +1 -0
- package/dist/modules-config.d.ts +3 -0
- package/dist/modules-config.d.ts.map +1 -0
- package/dist/modules-config.js +2 -0
- package/dist/modules-config.js.map +1 -0
- package/dist/pubsub/config.d.ts +11 -0
- package/dist/pubsub/config.d.ts.map +1 -0
- package/dist/pubsub/config.js +6 -0
- package/dist/pubsub/config.js.map +1 -0
- package/dist/pubsub/pubsub.d.ts +20 -0
- package/dist/pubsub/pubsub.d.ts.map +1 -0
- package/dist/pubsub/pubsub.js +54 -0
- package/dist/pubsub/pubsub.js.map +1 -0
- package/dist/pubsub/types.d.ts +24 -0
- package/dist/pubsub/types.d.ts.map +1 -0
- package/dist/pubsub/types.js +2 -0
- package/dist/pubsub/types.js.map +1 -0
- package/dist/rate-limit/config.d.ts +12 -0
- package/dist/rate-limit/config.d.ts.map +1 -0
- package/dist/rate-limit/config.js +6 -0
- package/dist/rate-limit/config.js.map +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +18 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +37 -0
- package/dist/rate-limit/rate-limiter.js.map +1 -0
- package/dist/rate-limit/types.d.ts +27 -0
- package/dist/rate-limit/types.d.ts.map +1 -0
- package/dist/rate-limit/types.js +2 -0
- package/dist/rate-limit/types.js.map +1 -0
- package/dist/redis/client-facade.d.ts +77 -0
- package/dist/redis/client-facade.d.ts.map +1 -0
- package/dist/redis/client-facade.js +102 -0
- package/dist/redis/client-facade.js.map +1 -0
- package/dist/redis/client.d.ts +10 -0
- package/dist/redis/client.d.ts.map +1 -0
- package/dist/redis/client.js +29 -0
- package/dist/redis/client.js.map +1 -0
- package/dist/redis/cluster.d.ts +7 -0
- package/dist/redis/cluster.d.ts.map +1 -0
- package/dist/redis/cluster.js +47 -0
- package/dist/redis/cluster.js.map +1 -0
- package/dist/redis/config.d.ts +39 -0
- package/dist/redis/config.d.ts.map +1 -0
- package/dist/redis/config.js +52 -0
- package/dist/redis/config.js.map +1 -0
- package/dist/redis/errors.d.ts +5 -0
- package/dist/redis/errors.d.ts.map +1 -0
- package/dist/redis/errors.js +5 -0
- package/dist/redis/errors.js.map +1 -0
- package/dist/redis/types.d.ts +135 -0
- package/dist/redis/types.d.ts.map +1 -0
- package/dist/redis/types.js +2 -0
- package/dist/redis/types.js.map +1 -0
- package/dist/redis/wrapper.d.ts +88 -0
- package/dist/redis/wrapper.d.ts.map +1 -0
- package/dist/redis/wrapper.js +206 -0
- package/dist/redis/wrapper.js.map +1 -0
- package/dist/session/config.d.ts +47 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +101 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/cookie.d.ts +16 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/cookie.js +28 -0
- package/dist/session/cookie.js.map +1 -0
- package/dist/session/errors.d.ts +56 -0
- package/dist/session/errors.d.ts.map +1 -0
- package/dist/session/errors.js +58 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/session/factory.d.ts +21 -0
- package/dist/session/factory.d.ts.map +1 -0
- package/dist/session/factory.js +30 -0
- package/dist/session/factory.js.map +1 -0
- package/dist/session/health.d.ts +12 -0
- package/dist/session/health.d.ts.map +1 -0
- package/dist/session/health.js +23 -0
- package/dist/session/health.js.map +1 -0
- package/dist/session/keys.d.ts +23 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +27 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/manager.d.ts +34 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +31 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/metrics.d.ts +11 -0
- package/dist/session/metrics.d.ts.map +1 -0
- package/dist/session/metrics.js +10 -0
- package/dist/session/metrics.js.map +1 -0
- package/dist/session/repository.d.ts +49 -0
- package/dist/session/repository.d.ts.map +1 -0
- package/dist/session/repository.js +203 -0
- package/dist/session/repository.js.map +1 -0
- package/dist/session/revocation.d.ts +22 -0
- package/dist/session/revocation.d.ts.map +1 -0
- package/dist/session/revocation.js +41 -0
- package/dist/session/revocation.js.map +1 -0
- package/dist/session/script-sources.d.ts +11 -0
- package/dist/session/script-sources.d.ts.map +1 -0
- package/dist/session/script-sources.js +140 -0
- package/dist/session/script-sources.js.map +1 -0
- package/dist/session/scripts.d.ts +15 -0
- package/dist/session/scripts.d.ts.map +1 -0
- package/dist/session/scripts.js +41 -0
- package/dist/session/scripts.js.map +1 -0
- package/dist/session/serializer.d.ts +12 -0
- package/dist/session/serializer.d.ts.map +1 -0
- package/dist/session/serializer.js +77 -0
- package/dist/session/serializer.js.map +1 -0
- package/dist/session/service.d.ts +48 -0
- package/dist/session/service.d.ts.map +1 -0
- package/dist/session/service.js +235 -0
- package/dist/session/service.js.map +1 -0
- package/dist/session/token.d.ts +16 -0
- package/dist/session/token.d.ts.map +1 -0
- package/dist/session/token.js +32 -0
- package/dist/session/token.js.map +1 -0
- package/dist/session/types.d.ts +134 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/streams/config.d.ts +12 -0
- package/dist/streams/config.d.ts.map +1 -0
- package/dist/streams/config.js +6 -0
- package/dist/streams/config.js.map +1 -0
- package/dist/streams/streams.d.ts +24 -0
- package/dist/streams/streams.d.ts.map +1 -0
- package/dist/streams/streams.js +55 -0
- package/dist/streams/streams.js.map +1 -0
- package/dist/streams/types.d.ts +32 -0
- package/dist/streams/types.d.ts.map +1 -0
- package/dist/streams/types.js +2 -0
- package/dist/streams/types.js.map +1 -0
- package/docs/ACCEPTANCE-REPORT.md +70 -0
- package/docs/ARCHITECTURE.md +61 -0
- package/docs/CAPACITY.md +33 -0
- package/docs/DEPLOYMENT.md +22 -0
- package/docs/README-API.md +15 -0
- package/docs/STATE-MACHINE.md +38 -0
- package/docs/TESTING.md +37 -0
- package/docs/THREAT-MODEL.md +23 -0
- package/docs/TYPE-SAFETY.md +34 -0
- package/docs/modules/cache/README.md +7 -0
- package/docs/modules/cache/usage.md +156 -0
- package/docs/modules/lock/README.md +7 -0
- package/docs/modules/lock/usage.md +105 -0
- package/docs/modules/pubsub/README.md +7 -0
- package/docs/modules/pubsub/usage.md +106 -0
- package/docs/modules/rate-limit/README.md +7 -0
- package/docs/modules/rate-limit/usage.md +100 -0
- package/docs/modules/sessions/README.md +7 -0
- package/docs/modules/sessions/usage.md +262 -0
- package/docs/modules/streams/README.md +7 -0
- package/docs/modules/streams/usage.md +141 -0
- package/package.json +50 -60
- package/src/scripts/cleanup-index.lua +4 -0
- package/src/scripts/conditional-update.lua +21 -0
- package/src/scripts/consume-session.lua +21 -0
- package/src/scripts/create-session.lua +28 -0
- package/src/scripts/delete.lua +2 -0
- package/src/scripts/destroy-user.lua +13 -0
- package/src/scripts/enforce-limit.lua +17 -0
- package/src/scripts/revoke-session.lua +13 -0
- package/src/scripts/rotate.lua +24 -0
- package/src/scripts/touch-session.lua +28 -0
- package/src/scripts/update-session.lua +18 -0
- package/dist/cache.d.ts +0 -796
- package/dist/cache.js +0 -1120
- package/dist/client.d.ts +0 -284
- package/dist/client.js +0 -1114
- package/dist/cluster-slot.d.ts +0 -4
- package/dist/cluster-slot.js +0 -31
- package/dist/cluster.d.ts +0 -79
- package/dist/cluster.js +0 -156
- package/dist/errors.d.ts +0 -30
- package/dist/errors.js +0 -63
- package/dist/health.d.ts +0 -180
- package/dist/health.js +0 -239
- package/dist/lock.d.ts +0 -248
- package/dist/lock.js +0 -397
- package/dist/logger.d.ts +0 -12
- package/dist/logger.js +0 -40
- package/dist/pubsub.d.ts +0 -423
- package/dist/pubsub.js +0 -537
- package/dist/ratelimiter.d.ts +0 -441
- package/dist/ratelimiter.js +0 -539
- package/dist/session/index.d.ts +0 -23
- package/dist/session/index.js +0 -16
- package/dist/session/revocation-store.d.ts +0 -176
- package/dist/session/revocation-store.js +0 -318
- package/dist/session/scripts/cleanup-index.lua +0 -21
- package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
- package/dist/session/scripts/conditional-update.lua +0 -63
- package/dist/session/scripts/create.lua +0 -83
- package/dist/session/scripts/delete-by-user.lua +0 -29
- package/dist/session/scripts/delete.lua +0 -15
- package/dist/session/scripts/enforce-limit.lua +0 -38
- package/dist/session/scripts/revoke.lua +0 -61
- package/dist/session/scripts/rotate-encrypted.lua +0 -110
- package/dist/session/scripts/rotate.lua +0 -122
- package/dist/session/scripts/touch-encrypted.lua +0 -89
- package/dist/session/scripts/touch.lua +0 -72
- package/dist/session/scripts/validate.lua +0 -90
- package/dist/session/session-circuit-breaker.d.ts +0 -42
- package/dist/session/session-circuit-breaker.js +0 -129
- package/dist/session/session-config.d.ts +0 -335
- package/dist/session/session-config.js +0 -162
- package/dist/session/session-cookie.d.ts +0 -72
- package/dist/session/session-cookie.js +0 -101
- package/dist/session/session-encryption.d.ts +0 -87
- package/dist/session/session-encryption.js +0 -139
- package/dist/session/session-errors.d.ts +0 -85
- package/dist/session/session-errors.js +0 -145
- package/dist/session/session-health.d.ts +0 -38
- package/dist/session/session-health.js +0 -60
- package/dist/session/session-keys.d.ts +0 -51
- package/dist/session/session-keys.js +0 -113
- package/dist/session/session-manager.d.ts +0 -73
- package/dist/session/session-manager.js +0 -94
- package/dist/session/session-metrics.d.ts +0 -33
- package/dist/session/session-metrics.js +0 -112
- package/dist/session/session-repository.d.ts +0 -161
- package/dist/session/session-repository.js +0 -683
- package/dist/session/session-scripts.d.ts +0 -36
- package/dist/session/session-scripts.js +0 -130
- package/dist/session/session-serializer.d.ts +0 -42
- package/dist/session/session-serializer.js +0 -248
- package/dist/session/session-service.d.ts +0 -104
- package/dist/session/session-service.js +0 -611
- package/dist/session/session-token.d.ts +0 -38
- package/dist/session/session-token.js +0 -86
- package/dist/session/session-types.d.ts +0 -253
- package/dist/session/session-types.js +0 -16
- package/dist/types.d.ts +0 -924
- package/dist/types.js +0 -151
- package/dist/utils/deepmerge.d.ts +0 -9
- package/dist/utils/deepmerge.js +0 -61
package/README.md
CHANGED
|
@@ -1,1101 +1,111 @@
|
|
|
1
1
|
# ioredis-toolkit
|
|
2
2
|
|
|
3
|
-
Production-
|
|
3
|
+
Production-oriented Redis infrastructure for Node.js 22+ and TypeScript, built around one shared Redis connection boundary.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
## Installation
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install ioredis-toolkit ioredis zod
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
<a name="quick-start"></a>
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { RedisClientWrapper, Cache, RateLimiter } from 'ioredis-toolkit';
|
|
17
|
-
|
|
18
|
-
// 1. Create the Redis client (standalone by default)
|
|
19
|
-
const client = new RedisClientWrapper({
|
|
20
|
-
mode: 'standalone',
|
|
21
|
-
host: 'localhost',
|
|
22
|
-
port: 6379,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// 2. Cache — JSON serialization, TTL, namespaces, compression
|
|
26
|
-
const cache = new Cache(client, { defaultTTL: 3600, compressionThreshold: 1024 });
|
|
27
|
-
await cache.set('user:1', { name: 'alice' });
|
|
28
|
-
const user = await cache.get('user:1');
|
|
29
|
-
|
|
30
|
-
// 3. Rate limiting — per-route, per-IP, per-user
|
|
31
|
-
const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
|
|
32
|
-
const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
|
|
33
|
-
if (!result.allowed) {
|
|
34
|
-
// HTTP 429, set Retry-After: result.retryAfter
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 4. Graceful shutdown
|
|
38
|
-
await client.close();
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
<a name="topology"></a>
|
|
42
|
-
## Topology & Configuration
|
|
43
|
-
|
|
44
|
-
Choose the Redis topology via the `mode` config field. All features behave identically across modes; only the underlying connection changes.
|
|
5
|
+
## Modules
|
|
45
6
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| `password` | `string` | — | Authentication password |
|
|
53
|
-
| `username` | `string` | — | Redis ACL username |
|
|
54
|
-
| `database` | `number` | `0` | Database index (standalone/sentinel only) |
|
|
55
|
-
| `sentinelNodes` | `Array<{host, port}>` | — | Sentinel nodes |
|
|
56
|
-
| `sentinelMasterName` | `string` | — | Sentinel master name |
|
|
57
|
-
| `clusterNodes` | `Array<{host, port}>` | — | Cluster nodes |
|
|
58
|
-
| `maxRetries` | `number` | `3` | Max reconnect attempts |
|
|
59
|
-
| `retryDelay` | `number` | `1000` | Base reconnect delay (ms) |
|
|
60
|
-
| `connectionTimeout` | `number` | `5000` | Connect timeout (ms) |
|
|
61
|
-
| `defaultTTL` | `number` | `3600` | Default cache TTL (seconds) |
|
|
62
|
-
| `compressionThreshold` | `number` | `1024` | Cache compression threshold (bytes) |
|
|
63
|
-
| `slowCommandThreshold` | `number` | `1000` | Log commands slower than this (ms) |
|
|
64
|
-
| `tls` | `boolean` | `false` | Enable TLS (`tlsOptions` for CA/cert/key) |
|
|
65
|
-
| `maxFanOutConcurrency` | `number` | `8` | Max concurrent fan-out operations in cluster |
|
|
66
|
-
| `maxBatchSize` | `number` | `500` | Max batch size for SCAN operations |
|
|
7
|
+
- Sessions — opaque credentials, rotation, revocation, idle/absolute expiry.
|
|
8
|
+
- Cache — JSON cache with TTL and conditional writes.
|
|
9
|
+
- Lock — ownership-token distributed leases.
|
|
10
|
+
- Rate limiting — atomic fixed-window counters.
|
|
11
|
+
- Pub/Sub — JSON fan-out over a dedicated subscriber connection.
|
|
12
|
+
- Streams — append, consumer groups, reads and acknowledgements.
|
|
67
13
|
|
|
68
|
-
|
|
14
|
+
## One client, all configuration
|
|
69
15
|
|
|
70
|
-
|
|
16
|
+
Every module can be configured in the same `createRedisClient()` call. Module configuration is optional and normalized independently.
|
|
71
17
|
|
|
72
|
-
|
|
18
|
+
Each module defaults to `enabled: false`. Accessing a module's getter on the client (`redis.cache`, `redis.lock`, `redis.rateLimiter`, `redis.pubsub`, `redis.streams`, `redis.sessions`) throws while it is disabled — set `enabled: true` in that module's configuration to use it.
|
|
73
19
|
|
|
74
20
|
```ts
|
|
75
|
-
|
|
21
|
+
import { createRedisClient } from 'ioredis-toolkit';
|
|
22
|
+
|
|
23
|
+
const redis = createRedisClient({
|
|
76
24
|
mode: 'standalone',
|
|
77
|
-
host: '
|
|
25
|
+
host: '127.0.0.1',
|
|
78
26
|
port: 6379,
|
|
79
|
-
password: 'secret',
|
|
80
|
-
database: 0,
|
|
81
|
-
});
|
|
82
27
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
const client = new RedisClientWrapper({
|
|
91
|
-
mode: 'sentinel',
|
|
92
|
-
sentinelNodes: [
|
|
93
|
-
{ host: 'sentinel1', port: 26379 },
|
|
94
|
-
{ host: 'sentinel2', port: 26380 },
|
|
95
|
-
],
|
|
96
|
-
sentinelMasterName: 'mymaster',
|
|
97
|
-
password: 'secret',
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
#### Cluster
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
const client = new RedisClientWrapper({
|
|
105
|
-
mode: 'cluster',
|
|
106
|
-
clusterNodes: [
|
|
107
|
-
{ host: 'redis1', port: 7000 },
|
|
108
|
-
{ host: 'redis2', port: 7001 },
|
|
109
|
-
{ host: 'redis3', port: 7002 },
|
|
110
|
-
],
|
|
111
|
-
password: 'secret',
|
|
28
|
+
cache: { enabled: true, namespace: 'app:cache', defaultTtl: 300 },
|
|
29
|
+
lock: { enabled: true, namespace: 'app:lock', defaultTtl: 30, maxTtl: 300 },
|
|
30
|
+
rateLimit: { enabled: true, namespace: 'app:limit', windowSeconds: 60, maxRequests: 100 },
|
|
31
|
+
pubsub: { enabled: true, channelPrefix: 'app:event' },
|
|
32
|
+
streams: { enabled: true, keyPrefix: 'app:stream', maxEntries: 100_000 },
|
|
33
|
+
sessions: { enabled: true, namespace: 'app:session' },
|
|
112
34
|
});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
<a name="redisclient"></a>
|
|
116
|
-
## RedisClientWrapper — The Unified Client
|
|
117
|
-
|
|
118
|
-
The `RedisClientWrapper` is the core of the library. It automatically adapts to the configured topology (standalone, sentinel, or cluster) and lazily creates and shares sub-components: `cache`, `pubsub`, `lock`, `rateLimiter`, and `session`.
|
|
119
|
-
|
|
120
|
-
### Flow Diagram
|
|
121
|
-
|
|
122
|
-
```text
|
|
123
|
-
+--------------------+ +----------------------+ +---------------------+
|
|
124
|
-
| RedisClientWrapper| --- | Sub-components | --- | Redis (underlying) |
|
|
125
|
-
| (config mode) | | cache/pubsub/lock | | ioredis client |
|
|
126
|
-
+--------------------+ +----------------------+ +---------------------+
|
|
127
|
-
^ ^ |
|
|
128
|
-
| | |
|
|
129
|
-
lazy init lazy init lazy init
|
|
130
|
-
| | |
|
|
131
|
-
+-----v------+ +-----v-------+ +-----v-------+
|
|
132
|
-
| get cache | | get lock | | get rateLimiter|
|
|
133
|
-
+------------+ +-------------+ +---------------+
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Convenience Accessors
|
|
137
|
-
|
|
138
|
-
`RedisClientWrapper` lazily creates and shares one instance of each sub-component. Access them as properties — no manual wiring needed:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
// Shared instances (created on first access)
|
|
142
|
-
client.cache; // Cache
|
|
143
|
-
client.pubsub; // PubSub
|
|
144
|
-
client.lock; // DistributedLock
|
|
145
|
-
client.rateLimiter; // RateLimiter
|
|
146
|
-
|
|
147
|
-
// Use them directly:
|
|
148
|
-
await client.cache.set('user:1', { name: 'alice' });
|
|
149
|
-
const ok = await client.lock.acquire('order:42');
|
|
150
|
-
const { allowed } = await client.rateLimiter.consume('/api', 'ip-1', { limit: 5, duration: 60 });
|
|
151
|
-
|
|
152
|
-
// Replace with custom instances:
|
|
153
|
-
client.cache = new Cache(client, { defaultTTL: 600 });
|
|
154
|
-
client.rateLimiter = new RateLimiter(client, { limit: 50, duration: 10 });
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Configuration
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
interface RedisClientOptions {
|
|
161
|
-
config: RedisConfigInput;
|
|
162
|
-
logger?: LoggerLike;
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Basic Commands
|
|
167
|
-
|
|
168
|
-
#### Strings & Keys
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
await client.set('name', 'alice'); // SET
|
|
172
|
-
await client.set('session', 'x', 3600); // SET ... EX
|
|
173
|
-
await client.setexnx('job:1', 'w', 60); // SET ... EX NX (only if missing)
|
|
174
|
-
await client.setnx('lock:1', 'owner', 30); // SETNX + EXPIRE, returns 1|0
|
|
175
|
-
await client.get('name'); // 'alice' | null
|
|
176
|
-
await client.getdel('queue:job'); // GETDEL
|
|
177
|
-
await client.exists('name'); // 1 | 0
|
|
178
|
-
await client.del('a', 'b'); // number deleted
|
|
179
|
-
await client.expire('session', 3600); // set TTL
|
|
180
|
-
await client.ttl('session'); // seconds left
|
|
181
|
-
await client.incr('visits'); // counters
|
|
182
|
-
await client.decr('stock:sku-1');
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### Batch Operations (Cluster-Safe)
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
await client.mset(['user:1', 'alice'], ['user:2', 'bob']); // grouped by hash slot
|
|
189
|
-
const [a, b] = await client.mget('user:1', 'user:2'); // routed per slot
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
#### Hashes, Sets, Sorted Sets
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
await client.hset('user:1', 'name', 'alice');
|
|
196
|
-
await client.hget('user:1', 'name');
|
|
197
|
-
await client.hgetall('user:1');
|
|
198
|
-
|
|
199
|
-
await client.sadd('tags:1', 'redis', 'typescript');
|
|
200
|
-
await client.smembers('tags:1');
|
|
201
|
-
await client.sismember('tags:1', 'redis');
|
|
202
|
-
await client.srem('tags:1', 'redis');
|
|
203
|
-
|
|
204
|
-
await client.zadd('leaderboard', 100, 'p1');
|
|
205
|
-
await client.zrange('leaderboard', 0, 9);
|
|
206
|
-
await client.zrem('leaderboard', 'p1');
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### Scanning & Pipelines
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
// Scans every node in cluster mode
|
|
213
|
-
for await (const key of client.scanIterator('session:*')) {
|
|
214
|
-
console.log(key);
|
|
215
|
-
}
|
|
216
|
-
await client.deletePattern('temp:*'); // delete by glob, cluster-safe
|
|
217
|
-
|
|
218
|
-
// Pipelines (cluster mode: keys must share a hash slot)
|
|
219
|
-
const pipeline = client.pipeline();
|
|
220
|
-
pipeline.set('a', '1');
|
|
221
|
-
pipeline.incr('b');
|
|
222
|
-
const results = await pipeline.exec();
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
#### Cluster Helpers
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
client.isCluster(); // boolean
|
|
229
|
-
client.getClusterNodes(); // raw node clients
|
|
230
|
-
client.getClusterSlots(); // raw slot map
|
|
231
|
-
client.getSlotRanges(); // Map<slot, host:port[]>
|
|
232
|
-
client.calculateSlot('{user}:a'); // CRC16 slot, honors hash tags
|
|
233
|
-
await client.getNodeForKey('user:1');// node serving a key
|
|
234
|
-
await client.isKeyServed('user:1'); // slot is served
|
|
235
|
-
await client.executeOnNode('user:1', 'get', 'user:1'); // run on owning node
|
|
236
|
-
await client.mgetClusterAware([...]); // slot-grouped multi-get
|
|
237
|
-
client.getClusterInfo(); // topology snapshot
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
#### Lua Scripts (Atomic Server-Side Logic)
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
// EVAL: the first numKeys arguments are KEYS, everything else is ARGV.
|
|
244
|
-
// Cluster mode: every key touched inside the script must be declared in KEYS and share one hash slot.
|
|
245
|
-
const script = `
|
|
246
|
-
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
247
|
-
return redis.call('DEL', KEYS[1])
|
|
248
|
-
end
|
|
249
|
-
return 0
|
|
250
|
-
`;
|
|
251
|
-
await client.set('lock:job', 'owner-1');
|
|
252
|
-
await client.eval(script, 1, 'lock:job', 'owner-1'); // 1 (deleted)
|
|
253
|
-
|
|
254
|
-
// SCRIPT LOAD + EVALSHA: avoid re-sending the script body on every call.
|
|
255
|
-
// evalsha falls back to EVAL automatically when the server cache was flushed.
|
|
256
|
-
const sha = await client.scriptLoad(script);
|
|
257
|
-
await client.evalsha(sha, script, 1, 'lock:job', 'owner-2'); // 0 (not owner)
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
#### Convenience Accessors (Session, PubSub, Lock, Cache)
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
client.cache; // shared Cache (created on first access)
|
|
264
|
-
client.pubsub; // shared PubSub
|
|
265
|
-
client.lock; // shared DistributedLock
|
|
266
|
-
client.rateLimiter; // shared RateLimiter
|
|
267
|
-
|
|
268
|
-
// Session subsystem:
|
|
269
|
-
const { token, session } = await manager.service.create({ userId: 'user-42' });
|
|
270
|
-
const result = await manager.service.validate(token, { userId: 'user-42' });
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Lifecycle
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
await client.ping(); // boolean
|
|
277
|
-
await client.close(); // graceful QUIT
|
|
278
|
-
client.raw; // raw ioredis client
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
<a name="cache"></a>
|
|
282
|
-
## Cache
|
|
283
|
-
|
|
284
|
-
The `Cache` layer provides JSON serialization, optional gzip compression, namespaces, TTLs, hash helpers, and pattern-based cleanup. Works in all three modes.
|
|
285
|
-
|
|
286
|
-
### Class: Cache
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
constructor(client: RedisClientWrapper, config: CacheInputConfig, logger: LoggerLike = defaultLogger)
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
| Param | Type | Description |
|
|
293
|
-
|---|---|---|
|
|
294
|
-
| `client` | `RedisClientWrapper` | The underlying Redis client |
|
|
295
|
-
| `config` | `CacheInputConfig` | `defaultTTL` (seconds) and `compressionThreshold` (bytes) |
|
|
296
|
-
| `logger` | `LoggerLike` | Optional pino-compatible logger; defaults to `console` |
|
|
297
|
-
|
|
298
|
-
#### basic usage
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
const cache = new Cache(client, { defaultTTL: 3600, compressionThreshold: 1024 });
|
|
302
|
-
|
|
303
|
-
await cache.set('user:1', { name: 'alice' }, { ttl: 300 });
|
|
304
|
-
const user = await cache.get('user:1');
|
|
305
|
-
|
|
306
|
-
await cache.delete('user:1');
|
|
307
|
-
await cache.exists('user:1');
|
|
308
|
-
await cache.expire('user:1', 60);
|
|
309
|
-
await cache.ttl('user:1');
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
#### Namespaces
|
|
313
|
-
|
|
314
|
-
Values stored under a namespace are prefixed with `namespace:`, keeping keys isolated.
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
await cache.set('token', 'abc', { namespace: 'auth' });
|
|
318
|
-
await cache.get('token', 'auth'); // 'abc'
|
|
319
|
-
await cache.get('token'); // null (no namespace)
|
|
320
|
-
|
|
321
|
-
await cache.clearNamespace('sessions'); // delete every 'sessions:*' key
|
|
322
|
-
await cache.keys('session:*'); // list keys (cluster-safe)
|
|
323
|
-
await cache.deletePattern('temp:*'); // delete by pattern
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Atomic & Batch Operations
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
await cache.setNX('job:1', 'worker-1', { ttl: 60 }); // only if missing
|
|
330
|
-
await cache.setEXNX('lock:1', 'txn', { ttl: 30 }); // atomic with TTL
|
|
331
|
-
|
|
332
|
-
await cache.increment('stats:visits'); // 1, 2, 3, ...
|
|
333
|
-
await cache.decrement('stock:sku-1');
|
|
334
|
-
|
|
335
|
-
await cache.mset({ 'user:1': alice, 'user:2': bob }, { ttl: 300 }); // slot-grouped
|
|
336
|
-
const [a, b] = await cache.mget(['user:1', 'user:2']);
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
#### Hash Helpers
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
await cache.hset('user:1', 'age', 30);
|
|
343
|
-
await cache.hget('user:1', 'age'); // 30
|
|
344
|
-
await cache.hgetall('user:1'); // { age: 30, ... }
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
#### Compression
|
|
348
|
-
|
|
349
|
-
Values larger than `compressionThreshold` bytes are gzip-compressed transparently. Set `compress: false` to disable compression for a single write.
|
|
350
|
-
|
|
351
|
-
```ts
|
|
352
|
-
await cache.set('large-data', bigBufferOrObject, { compress: false });
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
### Cache Interface — Method Documentation
|
|
356
|
-
|
|
357
|
-
| Method | Description | Args | Returns |
|
|
358
|
-
|---|---|---|---|
|
|
359
|
-
| `set(key, value, options?)` | Store a value in cache | `key: string`, `value: T`, `options: CacheOptions = {}` | `Promise<boolean>` — `true` when stored |
|
|
360
|
-
| `get(key, namespace?)` | Read a cached value | `key: string`, `namespace?: string` | `Promise<T \| null>` — parsed value or `null` |
|
|
361
|
-
| `setNX(key, value, options?)` | Store only if key does not exist | `key: string`, `value: T`, `options: CacheOptions = {}` | `Promise<boolean>` — `true` only when stored |
|
|
362
|
-
| `setEXNX(key, value, options?)` | Store atomically with TTL (SET ... EX NX) | `key: string`, `value: T`, `options: CacheOptions = {}` | `Promise<boolean>` — `true` only when stored |
|
|
363
|
-
| `mget(keys, namespace?)` | Read multiple keys (cluster-safe) | `keys: string[]`, `namespace?: string` | `Promise<(T \| null)[]>` — values in input order |
|
|
364
|
-
| `mset(entries, options?)` | Store multiple entries (cluster-safe, one pipeline per slot) | `entries: Record<string, T>`, `options: CacheOptions = {}` | `Promise<boolean>` — `true` when all stored |
|
|
365
|
-
| `delete(key, namespace?)` | Delete a cache key | `key: string`, `namespace?: string` | `Promise<boolean>` — `true` if key existed |
|
|
366
|
-
| `exists(key, namespace?)` | Check if key exists | `key: string`, `namespace?: string` | `Promise<boolean>` — `true` if key exists |
|
|
367
|
-
| `expire(key, ttl, namespace?)` | Set TTL on existing key | `key: string`, `ttl: number`, `namespace?: string` | `Promise<boolean>` — `true` if TTL applied |
|
|
368
|
-
| `ttl(key, namespace?)` | Get remaining TTL in seconds | `key: string`, `namespace?: string` | `Promise<number>` — seconds left (`-2` if missing, `-1` if no TTL) |
|
|
369
|
-
| `increment(key, by?, namespace?)` | Atomically increment counter | `key: string`, `by?: number` (default `1`), `namespace?: string` | `Promise<number>` — new counter value |
|
|
370
|
-
| `decrement(key, by?, namespace?)` | Atomically decrement counter | `key: string`, `by?: number` (default `1`), `namespace?: string` | `Promise<number>` — new counter value |
|
|
371
|
-
| `hget(key, field, namespace?)` | Read a hash field | `key: string`, `field: string`, `namespace?: string` | `Promise<T \| null>` — field value JSON-parsed or raw string |
|
|
372
|
-
| `hset(key, field, value, namespace?)` | Write a hash field | `key: string`, `field: string`, `value: any`, `namespace?: string` | `Promise<boolean>` — `true` if new field created |
|
|
373
|
-
| `hgetall(key, namespace?)` | Read all hash fields | `key: string`, `namespace?: string` | `Promise<Record<string, T>>` — field-value map JSON-parsed |
|
|
374
|
-
| `deletePattern(pattern, namespace?)` | Delete keys matching glob pattern (cluster-safe) | `pattern: string`, `namespace?: string` | `Promise<number>` — number of deleted keys |
|
|
375
|
-
| `keys(pattern, namespace?)` | List keys matching glob pattern (cluster-safe) | `pattern: string`, `namespace?: string` | `Promise<string[]>` — matching keys |
|
|
376
|
-
| `clearNamespace(namespace?)` | Delete every key inside a namespace | `namespace: string` | `Promise<number>` — number of deleted keys |
|
|
377
|
-
|
|
378
|
-
<a name="ratelimiter"></a>
|
|
379
|
-
## RateLimiter
|
|
380
|
-
|
|
381
|
-
Generic rate limiting for any resource — routes, API endpoints, users, IPs, API keys, database writes, email sends, webhooks...
|
|
382
|
-
|
|
383
|
-
### Algorithm Selection
|
|
384
|
-
|
|
385
|
-
| Algorithm | Key Type | Characteristics |
|
|
386
|
-
|---|---|---|
|
|
387
|
-
| `sliding` (default) | sorted set + atomic Lua | Smoothest; precise rolling window |
|
|
388
|
-
| `fixed` | counter (`INCR`/`EXPIRE`) | Cheapest; window resets at fixed boundaries |
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
// Sliding window (default)
|
|
392
|
-
const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
|
|
393
|
-
|
|
394
|
-
// Fixed window
|
|
395
|
-
const fixed = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### RateLimiter — Type Documentation
|
|
399
|
-
|
|
400
|
-
#### Class: RateLimiter
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
constructor(client: RedisClientWrapper, options: RateLimitOptionsInput = {}, logger: LoggerLike = defaultLogger)
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
| Param | Type | Default | Description |
|
|
407
|
-
|---|---|---|---|
|
|
408
|
-
| `client` | `RedisClientWrapper` | — | The underlying Redis client |
|
|
409
|
-
| `options.limit` | `number` | `100` | Maximum allowed requests within `duration` |
|
|
410
|
-
| `options.duration` | `number` | `60` | Window length in seconds |
|
|
411
|
-
| `options.algorithm` | `'fixed' \| 'sliding'` | `'sliding'` | Window algorithm |
|
|
412
|
-
| `options.namespace` | `string` | `'ratelimit'` | Redis key prefix |
|
|
413
|
-
|
|
414
|
-
#### RateLimitOptions type
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
interface RateLimitOptions {
|
|
418
|
-
limit?: number; // Max allowed requests within duration
|
|
419
|
-
duration?: number; // Window length in seconds
|
|
420
|
-
algorithm?: RateLimitAlgorithm; // 'fixed' | 'sliding'
|
|
421
|
-
namespace?: string; // Key prefix
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
#### RateLimitResult type
|
|
426
|
-
|
|
427
|
-
```ts
|
|
428
|
-
interface RateLimitResult {
|
|
429
|
-
allowed: boolean; // request may proceed
|
|
430
|
-
limit: number; // configured max
|
|
431
|
-
used: number; // requests in current window
|
|
432
|
-
remaining: number; // left in the window (limit - used, floored at 0)
|
|
433
|
-
resetAt: number; // epoch ms when the window resets
|
|
434
|
-
retryAfter: number; // seconds to wait (0 when allowed)
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
#### RateLimitAlgorithm type
|
|
439
|
-
|
|
440
|
-
```ts
|
|
441
|
-
type RateLimitAlgorithm = 'fixed' | 'sliding';
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### RateLimiter Methods
|
|
445
|
-
|
|
446
|
-
| Method | Description | Args | Returns |
|
|
447
|
-
|---|---|---|---|
|
|
448
|
-
| `consume(resource, identifier, options?)` | Consume one unit of capacity | `resource: string`, `identifier: string`, `options: RateLimitOptions = {}` | `Promise<RateLimitResult>` — limit state |
|
|
449
|
-
| `check(resource, identifier, options?)` | Peek at current limit state (no consumption) | `resource: string`, `identifier: string`, `options: RateLimitOptions = {}` | `Promise<RateLimitResult>` — current limit state |
|
|
450
|
-
| `reset(resource, identifier, namespace?)` | Reset counter, grant full capacity | `resource: string`, `identifier: string`, `namespace?: string` | `Promise<boolean>` — `true` if counter existed and was removed |
|
|
451
|
-
| `makeKey(resource, identifier, namespace?)` | Build the Redis key for a resource + identifier | `resource: string`, `identifier: string`, `namespace?: string` | `string` — e.g. `'ratelimit:/api/login:ip-10.0.0.1'` |
|
|
452
|
-
|
|
453
|
-
#### Private Methods (algorithmic)
|
|
454
|
-
|
|
455
|
-
| Method | Description |
|
|
456
|
-
|---|---|
|
|
457
|
-
| `consumeFixed(key, limit, duration)` | Fixed-window: `INCR`/`EXPIRE` based |
|
|
458
|
-
| `consumeSliding(key, limit, duration)` | Sliding-window: atomic Lua over sorted set |
|
|
459
|
-
| `checkFixed(key, limit, duration)` | Fixed-window peek |
|
|
460
|
-
| `checkSliding(key, limit, duration)` | Sliding-window peek |
|
|
461
|
-
|
|
462
|
-
<a name="lock"></a>
|
|
463
|
-
## DistributedLock
|
|
464
|
-
|
|
465
|
-
Atomic distributed mutual-exclusion lock backed by Redis. Works in standalone, sentinel, and cluster modes.
|
|
466
|
-
|
|
467
|
-
### Class: DistributedLock
|
|
468
|
-
|
|
469
|
-
```ts
|
|
470
|
-
constructor(client: RedisClientWrapper, logger: LoggerLike = defaultLogger, options: Partial<DistributedLockOptions> = {})
|
|
471
|
-
```
|
|
472
35
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
| `options.retryDelay` | `number` | `200` | Base delay between retries (ms), grows exponentially |
|
|
480
|
-
|
|
481
|
-
#### DistributedLockOptions type
|
|
482
|
-
|
|
483
|
-
```ts
|
|
484
|
-
type DistributedLockOptions = {
|
|
485
|
-
ttl?: number; // Lock TTL in milliseconds. Default: `30000`.
|
|
486
|
-
retryCount?: number; // Number of acquisition attempts. Default: `3`.
|
|
487
|
-
retryDelay?: number; // Base delay between retries in ms (grows exponentially). Default: `200`.
|
|
488
|
-
};
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
#### LockInfo type
|
|
492
|
-
|
|
493
|
-
```ts
|
|
494
|
-
type LockInfo = {
|
|
495
|
-
locked: boolean; // Whether the lock is currently held
|
|
496
|
-
ttl?: number; // Remaining TTL in seconds (when held and TTL set)
|
|
497
|
-
lockId?: string; // Unique owner id of the lock
|
|
498
|
-
};
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### DistributedLock Methods
|
|
502
|
-
|
|
503
|
-
| Method | Description | Args | Returns |
|
|
504
|
-
|---|---|---|---|
|
|
505
|
-
| `acquire(key, ttl?)` | Attempt to acquire the lock | `key: string`, `ttl?: number` (ms) | `Promise<boolean>` — `true` when acquired |
|
|
506
|
-
| `release(key)` | Release the lock (owner-checked) | `key: string` | `Promise<boolean>` — `true` if released, `false` if not owned or missing |
|
|
507
|
-
| `releaseForce(key)` | Force-release without ownership check | `key: string` | `Promise<boolean>` — `true` if a lock existed and was deleted |
|
|
508
|
-
| `extend(key, ttl?)` | Extend the lock TTL (owner-checked) | `key: string`, `ttl?: number` (ms) | `Promise<boolean>` — `true` if extended |
|
|
509
|
-
| `isLocked(key)` | Check if lock is held | `key: string` | `Promise<boolean>` — `true` if lock exists |
|
|
510
|
-
| `getLockInfo(key)` | Get lock details | `key: string` | `Promise<LockInfo>` — `{ locked, ttl, lockId }` |
|
|
511
|
-
| `getLockOwner(key)` | Get the lock owner ID | `key: string` | `Promise<string \| null>` — lock id or `null` |
|
|
512
|
-
| `getLockTTL(key)` | Get remaining TTL in seconds | `key: string` | `Promise<number>` — seconds left (`0` when not held or expired) |
|
|
513
|
-
| `withLock(key, fn, options?)` | Acquire lock, run critical section, auto-extend, always release | `key: string`, `fn: () => Promise<T>`, `options: DistributedLockOptions = {}` | `Promise<T>` — return value of `fn` |
|
|
514
|
-
| `cleanupAll()` | Delete every lock key (`lock:*`) from Redis | — | `Promise<number>` — number of deleted locks |
|
|
515
|
-
|
|
516
|
-
<a name="pubsub"></a>
|
|
517
|
-
## Pub/Sub
|
|
518
|
-
|
|
519
|
-
Redis Pub/Sub with a dedicated publisher and subscriber connection. Messages are JSON-serialized on publish and auto-parsed on delivery. Extends `EventEmitter` and emits `'error'` on subscriber failures.
|
|
520
|
-
|
|
521
|
-
### Class: PubSub
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
constructor(publisher: RedisClientWrapper, logger: LoggerLike = defaultLogger)
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
| Param | Type | Description |
|
|
528
|
-
|---|---|---|
|
|
529
|
-
| `publisher` | `RedisClientWrapper` | A Redis client used for publishing |
|
|
530
|
-
| `logger` | `LoggerLike` | Optional pino-compatible logger; defaults to `console` |
|
|
531
|
-
|
|
532
|
-
### PubSub — Type Documentation
|
|
533
|
-
|
|
534
|
-
#### Event Map
|
|
535
|
-
|
|
536
|
-
| Event | Payload |
|
|
537
|
-
|---|---|
|
|
538
|
-
| `message` | `{ channel: string, message: string }` |
|
|
539
|
-
| `pmessage` | `{ pattern: string, channel: string, message: string }` |
|
|
540
|
-
| `subscribe` | `{ channel: string, count: number }` |
|
|
541
|
-
| `unsubscribe` | `{ channel: string, count: number }` |
|
|
542
|
-
| `psubscribe` | `{ pattern: string, count: number }` |
|
|
543
|
-
| `punsubscribe` | `{ pattern: string, count: number }` |
|
|
544
|
-
| `error` | `Error` |
|
|
545
|
-
|
|
546
|
-
### PubSub Methods
|
|
547
|
-
|
|
548
|
-
| Method | Description | Args | Returns |
|
|
549
|
-
|---|---|---|---|
|
|
550
|
-
| `connectSubscriber(config)` | Open a dedicated subscriber connection (idempotent) | `config: RedisConfig` | `Promise<void>` |
|
|
551
|
-
| `publish(channel, message)` | Publish a message to a channel | `channel: string`, `message: T` (string or JSON-serializable) | `Promise<number>` — number of subscribers that received the message |
|
|
552
|
-
| `subscribe(channel, handler)` | Subscribe a handler to a channel | `channel: string`, `handler: (data: T) => void` | `Promise<void>` |
|
|
553
|
-
| `unsubscribe(channel, handler?)` | Remove a handler (or all handlers) from a channel | `channel: string`, `handler?: (data: T) => void` | `Promise<void>` |
|
|
554
|
-
| `psubscribe(pattern, handler)` | Subscribe to all channels matching a glob pattern | `pattern: string`, `handler: (data: { channel: string; message: T }) => void` | `Promise<void>` |
|
|
555
|
-
| `punsubscribe(pattern, handler?)` | Remove a handler from a pattern subscription | `pattern: string`, `handler?: (data: any) => void` | `Promise<void>` |
|
|
556
|
-
| `close()` | Close the subscriber connection and clear all subscriptions | — | `Promise<void>` — closes subscriber only; publisher is not closed |
|
|
557
|
-
| `getStats()` | Return subscription statistics | — | `PubSubStats` — `{ subscriptions, patternSubscriptions, connected }` |
|
|
558
|
-
|
|
559
|
-
#### PubSubStats type
|
|
560
|
-
|
|
561
|
-
```ts
|
|
562
|
-
type PubSubStats = {
|
|
563
|
-
subscriptions: number;
|
|
564
|
-
patternSubscriptions: number;
|
|
565
|
-
connected: boolean;
|
|
566
|
-
};
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
#### PubSubMessage type
|
|
570
|
-
|
|
571
|
-
```ts
|
|
572
|
-
type PubSubMessage<T = unknown> = {
|
|
573
|
-
channel: string;
|
|
574
|
-
message: T;
|
|
575
|
-
};
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
<a name="health"></a>
|
|
579
|
-
## HealthChecker
|
|
580
|
-
|
|
581
|
-
Periodic health monitoring with callbacks.
|
|
582
|
-
|
|
583
|
-
### Class: HealthChecker
|
|
584
|
-
|
|
585
|
-
```ts
|
|
586
|
-
constructor(client: RedisClientWrapper, logger: LoggerLike = defaultLogger)
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
| Param | Type | Description |
|
|
590
|
-
|---|---|---|
|
|
591
|
-
| `client` | `RedisClientWrapper` | The underlying Redis client |
|
|
592
|
-
| `logger` | `LoggerLike` | Optional pino-compatible logger; defaults to `console` |
|
|
593
|
-
|
|
594
|
-
### HealthChecker Methods
|
|
595
|
-
|
|
596
|
-
| Method | Description | Args | Returns |
|
|
597
|
-
|---|---|---|---|
|
|
598
|
-
| `start(interval?)` | Start periodic health checks | `interval?: number` (ms, default `10000`) | `void` |
|
|
599
|
-
| `stop()` | Stop the health checker | — | `void` |
|
|
600
|
-
| `check()` | Run a single health check (ping + latency) | — | `Promise<HealthStatus>` — current health status |
|
|
601
|
-
| `getStatus()` | Get the most recent health check result | — | `HealthStatus \| null` — last result (null before first check) |
|
|
602
|
-
| `onChange(callback)` | Register a callback for status changes | `callback: (status: HealthStatus) => void` | `void` |
|
|
603
|
-
| `waitForHealthy(timeout?)` | Wait until healthy (polling) | `timeout?: number` (ms, default `30000`) | `Promise<boolean>` — `true` if became healthy within timeout |
|
|
604
|
-
|
|
605
|
-
#### HealthStatus type
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
type HealthStatus = {
|
|
609
|
-
healthy: boolean;
|
|
610
|
-
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
611
|
-
latency: number; // ms
|
|
612
|
-
timestamp: Date;
|
|
613
|
-
details: {
|
|
614
|
-
ping: boolean;
|
|
615
|
-
connections?: number;
|
|
616
|
-
memory?: string;
|
|
617
|
-
};
|
|
618
|
-
};
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
<a name="session"></a>
|
|
622
|
-
## Session Subsystem
|
|
623
|
-
|
|
624
|
-
The production session stack (`createSessionManager`): validation with fail-closed semantics, rotation with retry-safe idempotency, throttled touches, idle/absolute expiry, per-user eviction ceilings, security versioning, optional AES-256-GCM encryption at rest, fail-closed circuit breaker, metrics and health. Cluster-safe by construction.
|
|
625
|
-
|
|
626
|
-
### Function: createSessionManager
|
|
627
|
-
|
|
628
|
-
```ts
|
|
629
|
-
createSessionManager(options: SessionManagerOptions): SessionManager
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
| Param | Type | Description |
|
|
633
|
-
|---|---|---|
|
|
634
|
-
| `options.client` | `RedisClientWrapper` | The underlying Redis client |
|
|
635
|
-
| `options.config` | `PartialSessionConfig` | Session configuration (see SessionConfig type) |
|
|
636
|
-
| `options.encryptionKeyProvider?` | `SessionKeyProvider` | REQUIRED when `config.encryption.enabled` is true |
|
|
637
|
-
| `options.revocationStore?` | `RevocationStore` | External revocation store (JWT jti denylists etc.) |
|
|
638
|
-
| `options.metricsAdapter?` | `SessionMetricsAdapter` | Metrics adapter (no-op without it) |
|
|
639
|
-
| `options.circuitBreaker?` | `SessionCircuitBreaker` | Optional circuit breaker |
|
|
640
|
-
| `options.now?` | `() => number` | Injectable clock for tests |
|
|
641
|
-
|
|
642
|
-
#### SessionManagerOptions type
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
type SessionManagerOptions = {
|
|
646
|
-
client: RedisClientWrapper;
|
|
647
|
-
config?: PartialSessionConfig;
|
|
648
|
-
encryptionKeyProvider?: SessionKeyProvider;
|
|
649
|
-
revocationStore?: RevocationStore;
|
|
650
|
-
metricsAdapter?: SessionMetricsAdapter;
|
|
651
|
-
circuitBreaker?: SessionCircuitBreaker;
|
|
652
|
-
now?: () => number;
|
|
653
|
-
}
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
#### WithSessionManagerOptions type
|
|
657
|
-
|
|
658
|
-
```ts
|
|
659
|
-
type WithSessionManagerOptions = {
|
|
660
|
-
config?: PartialSessionConfig;
|
|
661
|
-
encryptionKeyProvider?: SessionKeyProvider;
|
|
662
|
-
metricsAdapter?: SessionMetricsAdapter;
|
|
663
|
-
now?: () => number;
|
|
664
|
-
}
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### SessionManager class
|
|
668
|
-
|
|
669
|
-
```ts
|
|
670
|
-
new SessionManager(options: SessionManagerOptions)
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
Properties:
|
|
674
|
-
- `config: SessionConfig` — normalized configuration
|
|
675
|
-
- `service: SessionService` — the application-facing API
|
|
676
|
-
- `repository: SessionRepository` — low-level data access
|
|
677
|
-
- `metrics: SessionMetrics` — metrics tracking
|
|
678
|
-
- `circuitBreaker: SessionCircuitBreaker \| null` — circuit breaker (or null)
|
|
679
|
-
- `health: SessionHealthChecker` — health checking
|
|
680
|
-
- `cookies: SessionCookieManager` — cookie helpers
|
|
681
|
-
- `token: SessionTokenManager` — token generation/hashing
|
|
682
|
-
- `keys: SessionKeyStrategy` — key strategy for userId mapping
|
|
683
|
-
|
|
684
|
-
### SessionConfig type documentation
|
|
685
|
-
|
|
686
|
-
```ts
|
|
687
|
-
type SessionConfig = {
|
|
688
|
-
enabled: boolean; // Explicit opt-in; manager refuses to construct otherwise
|
|
689
|
-
namespace: string; // Key prefix (e.g. 'authcore')
|
|
690
|
-
ttl: number; // Absolute session lifetime in seconds (default: 2592000 = 30d)
|
|
691
|
-
idleTimeout: number | null; // Rolling idle timeout in seconds (default: 86400 = 24h, null disables)
|
|
692
|
-
rolling: boolean; // touch extends the idle boundary (default: true)
|
|
693
|
-
touchInterval: number; // Minimum seconds between touch writes (default: 300)
|
|
694
|
-
maxSessionsPerUser: number; // Eviction ceiling, enforced atomically (default: 20)
|
|
695
|
-
securityVersion: { // Global per-user version; bump invalidates older sessions
|
|
696
|
-
enabled: boolean;
|
|
697
|
-
};
|
|
698
|
-
encryption: { // AES-256-GCM envelopes
|
|
699
|
-
enabled: boolean;
|
|
700
|
-
encryptionKeyProvider: SessionKeyProvider;
|
|
701
|
-
};
|
|
702
|
-
jtiIndex: { // jti -> userId map so validate/touch/rotate work without userId
|
|
703
|
-
enabled: boolean;
|
|
704
|
-
};
|
|
705
|
-
checkRevocationStore: boolean; // Consult revocation store during validation (default: false)
|
|
706
|
-
bindingPolicy: 'disabled' | 'strict' | 'advisory'; // strict rejects on mismatch, advisory reports it
|
|
707
|
-
circuitBreaker: { // Circuit breaker config
|
|
708
|
-
failureThreshold: number; // default: 10
|
|
709
|
-
resetTimeoutMs: number; // default: 30_000
|
|
710
|
-
halfOpenMaxRequests: number; // default: 5
|
|
711
|
-
};
|
|
712
|
-
enableCreateIdempotency: boolean; // Idempotent create() via idempotencyKey (default: false)
|
|
713
|
-
retainConsumedTombstones: boolean; // Keep consumed records (TTL-bounded) for replay detection (default: true)
|
|
714
|
-
limits: { // Hard limits
|
|
715
|
-
maxMetadataSize: number; // max size of metadata in bytes
|
|
716
|
-
maxSessionsPerUserHardCap: number; // hard cap for listing sessions
|
|
717
|
-
};
|
|
718
|
-
health: { // Health check config
|
|
719
|
-
// ...
|
|
720
|
-
};
|
|
721
|
-
};
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
### Session Service Methods
|
|
725
|
-
|
|
726
|
-
| Method | Description | Args | Returns |
|
|
727
|
-
|---|---|---|---|
|
|
728
|
-
| `create(input)` | Create a session | `SessionCreateInput` | `Promise<CreatedSession>` — `{ token, session, replayed? }` |
|
|
729
|
-
| `validate(token, options?)` | Validate a session token | `token: string`, `options: ValidateOptions = {}` | `Promise<SessionValidationResult>` — `{ valid: true, session? }` or `{ valid: false, reason }` |
|
|
730
|
-
| `touch(token, options?)` | Refresh activity (throttled) | `token: string`, `options: TouchOptions = {}` | `Promise<TouchOutcome>` — outcome code |
|
|
731
|
-
| `rotate(token, options?)` | Rotate to a new session (idempotent via nonce) | `token: string`, `options: RotateOptions = {}` | `Promise<RotatedSession>` — `{ token?, session, replayed }` |
|
|
732
|
-
| `update(token, patch, options?)` | Patch update (optimistic concurrency) | `token: string`, `patch: SessionUpdatePatch`, `options: UpdateOptions = {}` | `Promise<SessionRecord>` |
|
|
733
|
-
| `destroy(token, options?)` | Physically delete a session (idempotent) | `token: string`, `options: { userId?: string } = {}` | `Promise<boolean>` |
|
|
734
|
-
| `revoke(token, options?)` | Logically revoke a session | `token: string`, `options: { userId?: string } = {}` | `Promise<string>` — outcome (`'revoked' \| 'already_revoked' \|\| 'not_found'`) |
|
|
735
|
-
| `revokeAll(userId)` | Revoke every session of a user | `userId: string` | `Promise<number>` — number revoked |
|
|
736
|
-
| `deleteByUser(userId)` | Delete every session of a user (physical) | `userId: string` | `Promise<string[]>` — deleted JTIs |
|
|
737
|
-
| `findByUser(userId, options?)` | List a user's sessions (oldest first) | `userId: string`, `options: ListOptions = {}` | `Promise<SessionRecord[]>` |
|
|
738
|
-
| `list(userId, options?)` | Alias of findByUser | `userId: string`, `options: ListOptions = {}` | `Promise<SessionRecord[]>` |
|
|
739
|
-
| `setSecurityVersion(userId, version?)` | Bump security version, invalidate old sessions | `userId: string`, `version?: number` | `Promise<number>` — new version |
|
|
740
|
-
| `getSecurityVersion(userId)` | Get current security version | `userId: string` | `Promise<number \| null>` |
|
|
741
|
-
| `health()` | Dependency health check | — | `Promise<ReturnType<SessionHealthChecker['check']>>` |
|
|
742
|
-
|
|
743
|
-
#### SessionCreateInput type
|
|
744
|
-
|
|
745
|
-
```ts
|
|
746
|
-
type SessionCreateInput = {
|
|
747
|
-
userId: string;
|
|
748
|
-
deviceId?: string; // stored when config.storeDeviceId is true
|
|
749
|
-
ipAddress?: string; // stored when config.storeIpAddress is true
|
|
750
|
-
userAgent?: string; // stored when config.storeUserAgent is true
|
|
751
|
-
metadata?: Record<string, unknown>; // bounded by config.maxMetadataSize
|
|
752
|
-
idempotencyKey?: string; // when provided + enableCreateIdempotency, enables idempotent create
|
|
753
|
-
};
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
#### CreatedSession type
|
|
757
|
-
|
|
758
|
-
```ts
|
|
759
|
-
type CreatedSession = {
|
|
760
|
-
token: string; // The raw session token. Give to client; store nowhere.
|
|
761
|
-
session: SessionRecord; // The persisted session record (contains only jti, never the token)
|
|
762
|
-
replayed?: boolean; // True when create was an idempotent replay
|
|
763
|
-
};
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
#### SessionValidationResult type
|
|
767
|
-
|
|
768
|
-
```ts
|
|
769
|
-
type SessionValidationResult =
|
|
770
|
-
| { valid: true; session: SessionRecord; binding?: BindingMismatch }
|
|
771
|
-
| { valid: false; reason: SessionInvalidReason; session?: never };
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
#### SessionInvalidReason type
|
|
775
|
-
|
|
776
|
-
```ts
|
|
777
|
-
type SessionInvalidReason =
|
|
778
|
-
| 'not_found'
|
|
779
|
-
| 'expired'
|
|
780
|
-
| 'idle_timeout'
|
|
781
|
-
| 'absolute_timeout'
|
|
782
|
-
| 'revoked'
|
|
783
|
-
| 'invalid'
|
|
784
|
-
| 'binding_mismatch';
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
#### TouchOutcome type
|
|
788
|
-
|
|
789
|
-
```ts
|
|
790
|
-
type TouchOutcome =
|
|
791
|
-
| 'touched'
|
|
792
|
-
| 'skipped_throttled'
|
|
793
|
-
| 'skipped_stale'
|
|
794
|
-
| 'not_found'
|
|
795
|
-
| 'consumed'
|
|
796
|
-
| 'expired'
|
|
797
|
-
| 'idle_expired';
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
#### TouchOptions type
|
|
801
|
-
|
|
802
|
-
```ts
|
|
803
|
-
type TouchOptions = {
|
|
804
|
-
force?: boolean; // Force a write regardless of touchInterval
|
|
805
|
-
userId?: string; // When known, avoids JTI lookup index round trip
|
|
806
|
-
};
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
#### RotateOptions type
|
|
810
|
-
|
|
811
|
-
```ts
|
|
812
|
-
type RotateOptions = {
|
|
813
|
-
rotationNonce?: string; // Client-supplied random nonce for retry-safe rotation
|
|
814
|
-
userId?: string; // Skip pre-flight GET, let Lua script be authoritative
|
|
815
|
-
expectedVersion?: number; // Optimistic concurrency: only rotate when version matches
|
|
816
|
-
};
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
#### UpdateOptions type
|
|
820
|
-
|
|
821
|
-
```ts
|
|
822
|
-
type UpdateOptions = {
|
|
823
|
-
expectedVersion?: number; // Optimistic concurrency: only update when version matches
|
|
824
|
-
userId?: string; // When known, avoids JTI lookup index round trip
|
|
825
|
-
};
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
#### ListOptions type
|
|
829
|
-
|
|
830
|
-
```ts
|
|
831
|
-
type ListOptions = {
|
|
832
|
-
limit?: number; // Max sessions to return (default: 100)
|
|
833
|
-
offset?: number; // Skip first N sessions (oldest first)
|
|
834
|
-
includeInactive?: boolean; // Include consumed/revoked records (default: false)
|
|
835
|
-
};
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
#### BindingMismatch type
|
|
839
|
-
|
|
840
|
-
```ts
|
|
841
|
-
type BindingMismatch = {
|
|
842
|
-
ipAddress: boolean; // IP address mismatch
|
|
843
|
-
userAgent: boolean; // User agent mismatch
|
|
844
|
-
deviceId: boolean; // Device ID mismatch
|
|
845
|
-
};
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
<a name="revocation"></a>
|
|
849
|
-
## RedisRevocationStore
|
|
850
|
-
|
|
851
|
-
Supports refresh-token revocation workflows: short-lived entries (`revoked:{jti}`) so rotated/logged-out tokens are rejected for their remaining lifetime. Framework-independent, works identically on standalone, Sentinel and Cluster, never stores raw tokens — only ids (`jti`).
|
|
852
|
-
|
|
853
|
-
### Class: RedisRevocationStore
|
|
854
|
-
|
|
855
|
-
```ts
|
|
856
|
-
new RedisRevocationStore(options: RedisRevocationStoreOptions)
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
| Param | Type | Description |
|
|
860
|
-
|---|---|---|
|
|
861
|
-
| `options.client` | `RedisClientWrapper` | The underlying Redis client |
|
|
862
|
-
| `options.keyPrefix` | `string` | Key prefix (e.g. `'authcore:revoked:'`) |
|
|
863
|
-
|
|
864
|
-
#### RedisRevocationStoreOptions type
|
|
865
|
-
|
|
866
|
-
```ts
|
|
867
|
-
interface RedisRevocationStoreOptions {
|
|
868
|
-
client: RedisClientWrapper;
|
|
869
|
-
keyPrefix: string;
|
|
870
|
-
}
|
|
36
|
+
await redis.cache.set('user:42', { id: '42' });
|
|
37
|
+
const lock = await redis.lock.acquire('job:42');
|
|
38
|
+
const decision = await redis.rateLimiter.consume('user:42');
|
|
39
|
+
await redis.pubsub.publish('orders.created', { orderId: '42' });
|
|
40
|
+
await redis.streams.add('orders', { orderId: '42', type: 'created' });
|
|
41
|
+
const session = await redis.sessions.create({ userId: '42' });
|
|
871
42
|
```
|
|
872
43
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
| Method | Description | Args | Returns |
|
|
876
|
-
|---|---|---|---|
|
|
877
|
-
| `revoke(record)` | Revoke a single jti | `record: RevocationRecord` | `Promise<void>` |
|
|
878
|
-
| `revokeMany(records)` | Revoke multiple jtis (batch) | `records: RevocationRecord[]` | `Promise<void>` — throws `RevocationBatchError` on failure |
|
|
879
|
-
| `isRevoked(jti)` | Check if a single jti is revoked | `jti: string` | `Promise<boolean>` |
|
|
880
|
-
| `isRevokedMany(jtis)` | Check multiple jtis (batch) | `jtis: string[]` | `Promise<Set<string>>` — set of revoked jtis that were found; throws `RevocationBatchError` on partial failure |
|
|
44
|
+
## Fluent module overrides
|
|
881
45
|
|
|
882
|
-
|
|
46
|
+
Each module can inherit the global configuration and override selected values:
|
|
883
47
|
|
|
884
48
|
```ts
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
49
|
+
redis
|
|
50
|
+
.withCache({ defaultTtl: 60 })
|
|
51
|
+
.withLock({ defaultTtl: 15 })
|
|
52
|
+
.withRateLimit({ maxRequests: 500 })
|
|
53
|
+
.withPubSub({ channelPrefix: 'critical-events' })
|
|
54
|
+
.withStreams({ maxEntries: 500_000 })
|
|
55
|
+
.withSessions({ idleTimeout: 60 * 60 });
|
|
890
56
|
```
|
|
891
57
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
Thrown when a batched command fails (Redis error, timeout, ...). Carries the exact jtis that failed — a check can never silently treat a token as "not revoked" when its status is unknown.
|
|
895
|
-
|
|
896
|
-
<a name="lua-scripts"></a>
|
|
897
|
-
## Lua Scripts (`eval` / `evalsha`)
|
|
898
|
-
|
|
899
|
-
Atomic server-side logic. Cluster-safe when keys share a hash slot.
|
|
900
|
-
|
|
901
|
-
### Usage
|
|
58
|
+
Use `'replace'` when the module should start again from its defaults:
|
|
902
59
|
|
|
903
60
|
```ts
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
907
|
-
return redis.call('DEL', KEYS[1])
|
|
908
|
-
end
|
|
909
|
-
return 0
|
|
910
|
-
`;
|
|
911
|
-
await client.set('lock:job', 'owner-1');
|
|
912
|
-
await client.eval(script, 1, 'lock:job', 'owner-1'); // 1 (deleted)
|
|
913
|
-
|
|
914
|
-
// SCRIPT LOAD + EVALSHA
|
|
915
|
-
const sha = await client.scriptLoad(script);
|
|
916
|
-
await client.evalsha(sha, script, 1, 'lock:job', 'owner-2'); // 0 (not owner)
|
|
61
|
+
redis.withCache({ namespace: 'temporary' }, 'replace');
|
|
62
|
+
redis.withLock({ namespace: 'maintenance' }, 'replace');
|
|
917
63
|
```
|
|
918
64
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
- **EVAL**: The first `numKeys` arguments are `KEYS`, everything else is `ARGV`.
|
|
922
|
-
- **Cluster mode**: Every key touched inside the script must be declared in `KEYS` and share one hash slot (honored via hash tags `{tag}:...`).
|
|
923
|
-
- **evalsha fallback**: If the script cache was flushed, `evalsha` automatically falls back to `EVAL`.
|
|
924
|
-
|
|
925
|
-
<a name="errors"></a>
|
|
926
|
-
## Errors
|
|
65
|
+
`withLock()` is canonical; `withlock()` is provided as an alias.
|
|
927
66
|
|
|
928
|
-
|
|
67
|
+
## Documentation
|
|
929
68
|
|
|
930
|
-
|
|
931
|
-
import { RedisError } from 'ioredis-toolkit';
|
|
932
|
-
|
|
933
|
-
try {
|
|
934
|
-
await client.select(1);
|
|
935
|
-
} catch (error) {
|
|
936
|
-
if (error instanceof RedisError && error.code === 'CLUSTER_MODE') {
|
|
937
|
-
// SELECT is not available in cluster mode
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
```
|
|
69
|
+
Each module has a complete usage guide with configuration tables, types, method arguments, return values, semantics, edge cases, and multiple examples:
|
|
941
70
|
|
|
942
|
-
|
|
71
|
+
- [Cache usage](./docs/modules/cache/usage.md)
|
|
72
|
+
- [Lock usage](./docs/modules/lock/usage.md)
|
|
73
|
+
- [Rate limiting usage](./docs/modules/rate-limit/usage.md)
|
|
74
|
+
- [Pub/Sub usage](./docs/modules/pubsub/usage.md)
|
|
75
|
+
- [Streams usage](./docs/modules/streams/usage.md)
|
|
76
|
+
- [Sessions usage](./docs/modules/sessions/usage.md)
|
|
943
77
|
|
|
944
|
-
|
|
945
|
-
|---|---|
|
|
946
|
-
| `CLUSTER_MODE` | Operations unavailable in cluster mode (e.g. `SELECT`) |
|
|
947
|
-
| `LOCK_ACQUISITION_FAILED` | Lock could not be acquired after retries |
|
|
948
|
-
| `LOCK_LOST` | Lock was lost during `withLock` execution |
|
|
949
|
-
| `SessionStorageError` | Infra failure (503) — only one that trips the circuit breaker |
|
|
950
|
-
| `SessionNotFoundError` | Session not found |
|
|
951
|
-
| `SessionExpiredError` | Session absolute TTL passed |
|
|
952
|
-
| `SessionInvalidError` | Session invalid (corrupt, cyclic metadata, etc.) |
|
|
953
|
-
| `SessionRevokedError` | Session explicitly revoked |
|
|
954
|
-
| `SessionRotationError` | Rotation failed (version conflict, successor collision, etc.) |
|
|
955
|
-
| `SessionConcurrencyError` | Optimistic concurrency violation (version mismatch) |
|
|
956
|
-
| `SessionSerializationError` | Session deserialization failed |
|
|
957
|
-
| `SessionConfigurationError` | Invalid session configuration |
|
|
958
|
-
| `SessionBindingError` | Binding policy mismatch (when strict) |
|
|
78
|
+
Additional architecture and operations documentation is in `docs/`.
|
|
959
79
|
|
|
960
|
-
|
|
961
|
-
## Logging
|
|
962
|
-
|
|
963
|
-
Every component accepts a pino-compatible logger (`trace/debug/info/warn/error/fatal` + `child`). Defaults to `console`.
|
|
80
|
+
## Installation
|
|
964
81
|
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
const logger = createLogger();
|
|
968
|
-
const client = new RedisClientWrapper(config, logger);
|
|
82
|
+
```bash
|
|
83
|
+
npm install ioredis-toolkit ioredis zod
|
|
969
84
|
```
|
|
970
85
|
|
|
971
|
-
|
|
972
|
-
## Mode Compatibility
|
|
86
|
+
The package is ESM-only and requires Node.js 22 or newer.
|
|
973
87
|
|
|
974
|
-
| Operation | Standalone | Sentinel | Cluster |
|
|
975
|
-
|---|---|---|---|
|
|
976
|
-
| Single-key commands (get/set/hash/set/zset/incr/...) | ✅ | ✅ | ✅ |
|
|
977
|
-
| `mget` / `mset` / `mgetClusterAware` | ✅ | ✅ | ✅ slot-grouped |
|
|
978
|
-
| `scanIterator` / `deletePattern` / `keys` | ✅ | ✅ | ✅ all nodes scanned |
|
|
979
|
-
| Pipelines | ✅ | ✅ | ✅ (same-slot keys per pipeline) |
|
|
980
|
-
| Lua scripts (`eval` / `evalsha` / `scriptLoad`) | ✅ | ✅ | ✅ (keys declared in `KEYS`, one slot) |
|
|
981
|
-
| `RedisRevocationStore` | ✅ | ✅ | ✅ batch ops slot-grouped |
|
|
982
|
-
| `createSessionManager` | ✅ | ✅ | ✅ hash-tagged Lua scripts, slot-grouped fan-out |
|
|
983
|
-
| `select(database)` | ✅ | ✅ | ❌ (Redis limitation) |
|
|
984
|
-
| Hash-tag keys `{tag}:...` | ✅ | ✅ | ✅ same slot |
|
|
985
|
-
|
|
986
|
-
<a name="development"></a>
|
|
987
88
|
## Development
|
|
988
89
|
|
|
90
|
+
This repo uses [Bun](https://bun.sh) for dependency installation and scripts; the committed `bun.lock` is the source of truth.
|
|
91
|
+
|
|
989
92
|
```bash
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
93
|
+
bun install
|
|
94
|
+
bun run typecheck
|
|
95
|
+
bun run lint
|
|
96
|
+
bun run test
|
|
97
|
+
bun run test:integration
|
|
98
|
+
bun run build
|
|
996
99
|
```
|
|
997
100
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
<a name="types"></a>
|
|
1001
|
-
## Type Exports
|
|
1002
|
-
|
|
1003
|
-
The package exports comprehensive types for all modules. Key type exports:
|
|
1004
|
-
|
|
1005
|
-
### Core Client Types
|
|
1006
|
-
|
|
1007
|
-
| Type | Description |
|
|
1008
|
-
|---|---|
|
|
1009
|
-
| `RedisClientWrapper` | The unified client wrapper (standalone/sentinel/cluster) |
|
|
1010
|
-
| `createRedisClient` | Factory function: `createRedisClient(config)` — creates client for specified mode |
|
|
1011
|
-
| `RedisConfig` | Normalized Redis configuration (after Zod validation) |
|
|
1012
|
-
| `RedisConfigInput` | User-facing configuration input (validated with Zod) |
|
|
1013
|
-
| `RedisMode` | `'standalone' \| 'sentinel' \| 'cluster'` |
|
|
1014
|
-
| `RedisConfigForMode<M>` | Mode-specific config type |
|
|
1015
|
-
|
|
1016
|
-
### Cache Types
|
|
1017
|
-
|
|
1018
|
-
| Type | Description |
|
|
1019
|
-
|---|---|
|
|
1020
|
-
| `Cache` | Cache layer with JSON serialization, TTL, namespaces, compression |
|
|
1021
|
-
| `CacheOptions` | Options for cache operations (`ttl`, `compress`, `namespace`) |
|
|
1022
|
-
| `CacheInputConfig` | Constructor config (`defaultTTL`, `compressionThreshold`) |
|
|
1023
|
-
|
|
1024
|
-
### Rate Limiting Types
|
|
1025
|
-
|
|
1026
|
-
| Type | Description |
|
|
1027
|
-
|---|---|
|
|
1028
|
-
| `RateLimiter` | Rate limiter instance |
|
|
1029
|
-
| `RateLimitAlgorithm` | `'fixed' \| 'sliding'` |
|
|
1030
|
-
| `RateLimitOptions` | Options with defaults materialized (limit, duration, algorithm, namespace) |
|
|
1031
|
-
| `RateLimitResult` | Result of consume/check: allowed, limit, used, remaining, resetAt, retryAfter |
|
|
1032
|
-
|
|
1033
|
-
### Distributed Lock Types
|
|
1034
|
-
|
|
1035
|
-
| Type | Description |
|
|
1036
|
-
|---|---|
|
|
1037
|
-
| `DistributedLock` | Distributed lock instance |
|
|
1038
|
-
| `DistributedLockOptions` | Constructor options (ttl, retryCount, retryDelay) |
|
|
1039
|
-
| `LockInfo` | Lock info: `{ locked, ttl?, lockId? }` |
|
|
1040
|
-
|
|
1041
|
-
### Pub/Sub Types
|
|
1042
|
-
|
|
1043
|
-
| Type | Description |
|
|
1044
|
-
|---|---|
|
|
1045
|
-
| `PubSub` | Pub/sub instance |
|
|
1046
|
-
| `PubSubMessage<T>` | `{ channel: string; message: T }` |
|
|
1047
|
-
| `PubSubStats` | `{ subscriptions, patternSubscriptions, connected }` |
|
|
1048
|
-
|
|
1049
|
-
### Health Types
|
|
1050
|
-
|
|
1051
|
-
| Type | Description |
|
|
1052
|
-
|---|---|
|
|
1053
|
-
| `HealthStatus` | `{ healthy, status, latency, timestamp, details }` |
|
|
1054
|
-
|
|
1055
|
-
### Session Types
|
|
1056
|
-
|
|
1057
|
-
| Type | Description |
|
|
1058
|
-
|---|---|
|
|
1059
|
-
| `SessionManager` | Session manager composition root |
|
|
1060
|
-
| `SessionService` | Application-facing session API |
|
|
1061
|
-
| `SessionRecord` | Persisted session record |
|
|
1062
|
-
| `SessionCreateInput` | Input for `create()` |
|
|
1063
|
-
| `CreatedSession` | Result of `create()`: `{ token, session, replayed? }` |
|
|
1064
|
-
| `SessionValidationResult` | Result of `validate()` |
|
|
1065
|
-
| `SessionInvalidReason` | Invalid reason discriminant |
|
|
1066
|
-
| `TouchOutcome` | Touch outcome codes |
|
|
1067
|
-
| `RotateOptions` | Options for `rotate()` |
|
|
1068
|
-
| `UpdateOptions` | Options for `update()` |
|
|
1069
|
-
| `ListOptions` | Options for `findByUser`/`list()` |
|
|
1070
|
-
| `BindingMismatch` | Binding mismatch details |
|
|
1071
|
-
| `SessionStatus` | `'active' \| 'consumed' \| 'revoked'` |
|
|
1072
|
-
| `SessionStatus` | Session lifecycle state |
|
|
1073
|
-
| `RevocationRecord` | Revocation store record |
|
|
1074
|
-
| `RevocationStore` | Storage-agnostic revocation interface |
|
|
1075
|
-
|
|
1076
|
-
### Configuration Types
|
|
1077
|
-
|
|
1078
|
-
| Type | Description |
|
|
1079
|
-
|---|---|
|
|
1080
|
-
| `RedisCommonConfig` | Common config shared by all topologies |
|
|
1081
|
-
| `StandaloneRedisConfig` | Standalone-specific config |
|
|
1082
|
-
| `SentinelRedisConfig` | Sentinel-specific config |
|
|
1083
|
-
| `ClusterRedisConfig` | Cluster-specific config |
|
|
1084
|
-
| `RedisConfigInputSchema` | Zod schema for config validation |
|
|
1085
|
-
| `BaseRedisConfigSchema` | Base config schema (password, username, database, tls, etc.) |
|
|
1086
|
-
|
|
1087
|
-
### Utility Types
|
|
101
|
+
Real Redis integration tests require a Redis deployment. Docker definitions are provided under `docker/`.
|
|
1088
102
|
|
|
1089
|
-
|
|
1090
|
-
|---|---|
|
|
1091
|
-
| `RedisError` | Base Redis error type |
|
|
1092
|
-
| `calculateRedisClusterSlot` | CRC16 slot calculation for cluster keys |
|
|
1093
|
-
| `hashTag` | Extract hash tag from key (`{tag}:key`) |
|
|
103
|
+
## Design principle
|
|
1094
104
|
|
|
1095
|
-
|
|
1096
|
-
## Changelog
|
|
105
|
+
The session layer and other modules use the shared Redis abstraction rather than creating competing clients. Redis Cluster is treated as a distributed keyspace: same-slot atomicity is used only where Redis can actually provide it, while cross-slot work uses explicit bounded fan-out.
|
|
1097
106
|
|
|
1098
|
-
|
|
107
|
+
## Links
|
|
1099
108
|
|
|
1100
|
-
|
|
1101
|
-
|
|
109
|
+
- [Changelog](./CHANGELOG.md)
|
|
110
|
+
- [Issues](https://github.com/org-utils/ioredis-toolkit/issues)
|
|
111
|
+
- [Repository](https://github.com/org-utils/ioredis-toolkit)
|