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.
Files changed (267) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +68 -1058
  4. package/dist/cache/cache.d.ts +30 -0
  5. package/dist/cache/cache.d.ts.map +1 -0
  6. package/dist/cache/cache.js +59 -0
  7. package/dist/cache/cache.js.map +1 -0
  8. package/dist/cache/config.d.ts +12 -0
  9. package/dist/cache/config.d.ts.map +1 -0
  10. package/dist/cache/config.js +13 -0
  11. package/dist/cache/config.js.map +1 -0
  12. package/dist/cache/types.d.ts +32 -0
  13. package/dist/cache/types.d.ts.map +1 -0
  14. package/dist/cache/types.js +5 -0
  15. package/dist/cache/types.js.map +1 -0
  16. package/dist/index.d.ts +43 -51
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +32 -44
  19. package/dist/index.js.map +1 -0
  20. package/dist/lock/config.d.ts +12 -0
  21. package/dist/lock/config.d.ts.map +1 -0
  22. package/dist/lock/config.js +8 -0
  23. package/dist/lock/config.js.map +1 -0
  24. package/dist/lock/lock.d.ts +20 -0
  25. package/dist/lock/lock.d.ts.map +1 -0
  26. package/dist/lock/lock.js +44 -0
  27. package/dist/lock/lock.js.map +1 -0
  28. package/dist/lock/types.d.ts +19 -0
  29. package/dist/lock/types.d.ts.map +1 -0
  30. package/dist/lock/types.js +2 -0
  31. package/dist/lock/types.js.map +1 -0
  32. package/dist/modules-config.d.ts +3 -0
  33. package/dist/modules-config.d.ts.map +1 -0
  34. package/dist/modules-config.js +2 -0
  35. package/dist/modules-config.js.map +1 -0
  36. package/dist/pubsub/config.d.ts +11 -0
  37. package/dist/pubsub/config.d.ts.map +1 -0
  38. package/dist/pubsub/config.js +6 -0
  39. package/dist/pubsub/config.js.map +1 -0
  40. package/dist/pubsub/pubsub.d.ts +20 -0
  41. package/dist/pubsub/pubsub.d.ts.map +1 -0
  42. package/dist/pubsub/pubsub.js +54 -0
  43. package/dist/pubsub/pubsub.js.map +1 -0
  44. package/dist/pubsub/types.d.ts +24 -0
  45. package/dist/pubsub/types.d.ts.map +1 -0
  46. package/dist/pubsub/types.js +2 -0
  47. package/dist/pubsub/types.js.map +1 -0
  48. package/dist/rate-limit/config.d.ts +12 -0
  49. package/dist/rate-limit/config.d.ts.map +1 -0
  50. package/dist/rate-limit/config.js +6 -0
  51. package/dist/rate-limit/config.js.map +1 -0
  52. package/dist/rate-limit/rate-limiter.d.ts +18 -0
  53. package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
  54. package/dist/rate-limit/rate-limiter.js +37 -0
  55. package/dist/rate-limit/rate-limiter.js.map +1 -0
  56. package/dist/rate-limit/types.d.ts +27 -0
  57. package/dist/rate-limit/types.d.ts.map +1 -0
  58. package/dist/rate-limit/types.js +2 -0
  59. package/dist/rate-limit/types.js.map +1 -0
  60. package/dist/redis/client-facade.d.ts +77 -0
  61. package/dist/redis/client-facade.d.ts.map +1 -0
  62. package/dist/redis/client-facade.js +102 -0
  63. package/dist/redis/client-facade.js.map +1 -0
  64. package/dist/redis/client.d.ts +10 -0
  65. package/dist/redis/client.d.ts.map +1 -0
  66. package/dist/redis/client.js +29 -0
  67. package/dist/redis/client.js.map +1 -0
  68. package/dist/redis/cluster.d.ts +7 -0
  69. package/dist/redis/cluster.d.ts.map +1 -0
  70. package/dist/redis/cluster.js +47 -0
  71. package/dist/redis/cluster.js.map +1 -0
  72. package/dist/redis/config.d.ts +39 -0
  73. package/dist/redis/config.d.ts.map +1 -0
  74. package/dist/redis/config.js +52 -0
  75. package/dist/redis/config.js.map +1 -0
  76. package/dist/redis/errors.d.ts +5 -0
  77. package/dist/redis/errors.d.ts.map +1 -0
  78. package/dist/redis/errors.js +5 -0
  79. package/dist/redis/errors.js.map +1 -0
  80. package/dist/redis/types.d.ts +135 -0
  81. package/dist/redis/types.d.ts.map +1 -0
  82. package/dist/redis/types.js +2 -0
  83. package/dist/redis/types.js.map +1 -0
  84. package/dist/redis/wrapper.d.ts +88 -0
  85. package/dist/redis/wrapper.d.ts.map +1 -0
  86. package/dist/redis/wrapper.js +206 -0
  87. package/dist/redis/wrapper.js.map +1 -0
  88. package/dist/session/config.d.ts +47 -0
  89. package/dist/session/config.d.ts.map +1 -0
  90. package/dist/session/config.js +101 -0
  91. package/dist/session/config.js.map +1 -0
  92. package/dist/session/cookie.d.ts +16 -0
  93. package/dist/session/cookie.d.ts.map +1 -0
  94. package/dist/session/cookie.js +28 -0
  95. package/dist/session/cookie.js.map +1 -0
  96. package/dist/session/errors.d.ts +56 -0
  97. package/dist/session/errors.d.ts.map +1 -0
  98. package/dist/session/errors.js +58 -0
  99. package/dist/session/errors.js.map +1 -0
  100. package/dist/session/factory.d.ts +21 -0
  101. package/dist/session/factory.d.ts.map +1 -0
  102. package/dist/session/factory.js +30 -0
  103. package/dist/session/factory.js.map +1 -0
  104. package/dist/session/health.d.ts +12 -0
  105. package/dist/session/health.d.ts.map +1 -0
  106. package/dist/session/health.js +23 -0
  107. package/dist/session/health.js.map +1 -0
  108. package/dist/session/keys.d.ts +23 -0
  109. package/dist/session/keys.d.ts.map +1 -0
  110. package/dist/session/keys.js +27 -0
  111. package/dist/session/keys.js.map +1 -0
  112. package/dist/session/manager.d.ts +34 -0
  113. package/dist/session/manager.d.ts.map +1 -0
  114. package/dist/session/manager.js +31 -0
  115. package/dist/session/manager.js.map +1 -0
  116. package/dist/session/metrics.d.ts +11 -0
  117. package/dist/session/metrics.d.ts.map +1 -0
  118. package/dist/session/metrics.js +10 -0
  119. package/dist/session/metrics.js.map +1 -0
  120. package/dist/session/repository.d.ts +49 -0
  121. package/dist/session/repository.d.ts.map +1 -0
  122. package/dist/session/repository.js +203 -0
  123. package/dist/session/repository.js.map +1 -0
  124. package/dist/session/revocation.d.ts +22 -0
  125. package/dist/session/revocation.d.ts.map +1 -0
  126. package/dist/session/revocation.js +41 -0
  127. package/dist/session/revocation.js.map +1 -0
  128. package/dist/session/script-sources.d.ts +11 -0
  129. package/dist/session/script-sources.d.ts.map +1 -0
  130. package/dist/session/script-sources.js +140 -0
  131. package/dist/session/script-sources.js.map +1 -0
  132. package/dist/session/scripts.d.ts +15 -0
  133. package/dist/session/scripts.d.ts.map +1 -0
  134. package/dist/session/scripts.js +41 -0
  135. package/dist/session/scripts.js.map +1 -0
  136. package/dist/session/serializer.d.ts +12 -0
  137. package/dist/session/serializer.d.ts.map +1 -0
  138. package/dist/session/serializer.js +77 -0
  139. package/dist/session/serializer.js.map +1 -0
  140. package/dist/session/service.d.ts +48 -0
  141. package/dist/session/service.d.ts.map +1 -0
  142. package/dist/session/service.js +235 -0
  143. package/dist/session/service.js.map +1 -0
  144. package/dist/session/token.d.ts +16 -0
  145. package/dist/session/token.d.ts.map +1 -0
  146. package/dist/session/token.js +32 -0
  147. package/dist/session/token.js.map +1 -0
  148. package/dist/session/types.d.ts +134 -0
  149. package/dist/session/types.d.ts.map +1 -0
  150. package/dist/session/types.js +2 -0
  151. package/dist/session/types.js.map +1 -0
  152. package/dist/streams/config.d.ts +12 -0
  153. package/dist/streams/config.d.ts.map +1 -0
  154. package/dist/streams/config.js +6 -0
  155. package/dist/streams/config.js.map +1 -0
  156. package/dist/streams/streams.d.ts +24 -0
  157. package/dist/streams/streams.d.ts.map +1 -0
  158. package/dist/streams/streams.js +55 -0
  159. package/dist/streams/streams.js.map +1 -0
  160. package/dist/streams/types.d.ts +32 -0
  161. package/dist/streams/types.d.ts.map +1 -0
  162. package/dist/streams/types.js +2 -0
  163. package/dist/streams/types.js.map +1 -0
  164. package/docs/ACCEPTANCE-REPORT.md +70 -0
  165. package/docs/ARCHITECTURE.md +61 -0
  166. package/docs/CAPACITY.md +33 -0
  167. package/docs/DEPLOYMENT.md +22 -0
  168. package/docs/README-API.md +15 -0
  169. package/docs/STATE-MACHINE.md +38 -0
  170. package/docs/TESTING.md +37 -0
  171. package/docs/THREAT-MODEL.md +23 -0
  172. package/docs/TYPE-SAFETY.md +34 -0
  173. package/docs/modules/cache/README.md +7 -0
  174. package/docs/modules/cache/usage.md +156 -0
  175. package/docs/modules/lock/README.md +7 -0
  176. package/docs/modules/lock/usage.md +105 -0
  177. package/docs/modules/pubsub/README.md +7 -0
  178. package/docs/modules/pubsub/usage.md +106 -0
  179. package/docs/modules/rate-limit/README.md +7 -0
  180. package/docs/modules/rate-limit/usage.md +100 -0
  181. package/docs/modules/sessions/README.md +7 -0
  182. package/docs/modules/sessions/usage.md +262 -0
  183. package/docs/modules/streams/README.md +7 -0
  184. package/docs/modules/streams/usage.md +141 -0
  185. package/package.json +50 -60
  186. package/src/scripts/cleanup-index.lua +4 -0
  187. package/src/scripts/conditional-update.lua +21 -0
  188. package/src/scripts/consume-session.lua +21 -0
  189. package/src/scripts/create-session.lua +28 -0
  190. package/src/scripts/delete.lua +2 -0
  191. package/src/scripts/destroy-user.lua +13 -0
  192. package/src/scripts/enforce-limit.lua +17 -0
  193. package/src/scripts/revoke-session.lua +13 -0
  194. package/src/scripts/rotate.lua +24 -0
  195. package/src/scripts/touch-session.lua +28 -0
  196. package/src/scripts/update-session.lua +18 -0
  197. package/dist/cache.d.ts +0 -796
  198. package/dist/cache.js +0 -1120
  199. package/dist/client.d.ts +0 -284
  200. package/dist/client.js +0 -1114
  201. package/dist/cluster-slot.d.ts +0 -4
  202. package/dist/cluster-slot.js +0 -31
  203. package/dist/cluster.d.ts +0 -79
  204. package/dist/cluster.js +0 -156
  205. package/dist/errors.d.ts +0 -30
  206. package/dist/errors.js +0 -63
  207. package/dist/health.d.ts +0 -180
  208. package/dist/health.js +0 -239
  209. package/dist/lock.d.ts +0 -248
  210. package/dist/lock.js +0 -397
  211. package/dist/logger.d.ts +0 -12
  212. package/dist/logger.js +0 -40
  213. package/dist/pubsub.d.ts +0 -423
  214. package/dist/pubsub.js +0 -537
  215. package/dist/ratelimiter.d.ts +0 -441
  216. package/dist/ratelimiter.js +0 -539
  217. package/dist/session/index.d.ts +0 -23
  218. package/dist/session/index.js +0 -16
  219. package/dist/session/revocation-store.d.ts +0 -176
  220. package/dist/session/revocation-store.js +0 -318
  221. package/dist/session/scripts/cleanup-index.lua +0 -21
  222. package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
  223. package/dist/session/scripts/conditional-update.lua +0 -63
  224. package/dist/session/scripts/create.lua +0 -83
  225. package/dist/session/scripts/delete-by-user.lua +0 -29
  226. package/dist/session/scripts/delete.lua +0 -15
  227. package/dist/session/scripts/enforce-limit.lua +0 -38
  228. package/dist/session/scripts/revoke.lua +0 -61
  229. package/dist/session/scripts/rotate-encrypted.lua +0 -110
  230. package/dist/session/scripts/rotate.lua +0 -122
  231. package/dist/session/scripts/touch-encrypted.lua +0 -89
  232. package/dist/session/scripts/touch.lua +0 -72
  233. package/dist/session/scripts/validate.lua +0 -90
  234. package/dist/session/session-circuit-breaker.d.ts +0 -42
  235. package/dist/session/session-circuit-breaker.js +0 -129
  236. package/dist/session/session-config.d.ts +0 -335
  237. package/dist/session/session-config.js +0 -162
  238. package/dist/session/session-cookie.d.ts +0 -72
  239. package/dist/session/session-cookie.js +0 -101
  240. package/dist/session/session-encryption.d.ts +0 -87
  241. package/dist/session/session-encryption.js +0 -139
  242. package/dist/session/session-errors.d.ts +0 -85
  243. package/dist/session/session-errors.js +0 -145
  244. package/dist/session/session-health.d.ts +0 -38
  245. package/dist/session/session-health.js +0 -60
  246. package/dist/session/session-keys.d.ts +0 -51
  247. package/dist/session/session-keys.js +0 -113
  248. package/dist/session/session-manager.d.ts +0 -73
  249. package/dist/session/session-manager.js +0 -94
  250. package/dist/session/session-metrics.d.ts +0 -33
  251. package/dist/session/session-metrics.js +0 -112
  252. package/dist/session/session-repository.d.ts +0 -161
  253. package/dist/session/session-repository.js +0 -683
  254. package/dist/session/session-scripts.d.ts +0 -36
  255. package/dist/session/session-scripts.js +0 -130
  256. package/dist/session/session-serializer.d.ts +0 -42
  257. package/dist/session/session-serializer.js +0 -248
  258. package/dist/session/session-service.d.ts +0 -104
  259. package/dist/session/session-service.js +0 -611
  260. package/dist/session/session-token.d.ts +0 -38
  261. package/dist/session/session-token.js +0 -86
  262. package/dist/session/session-types.d.ts +0 -253
  263. package/dist/session/session-types.js +0 -16
  264. package/dist/types.d.ts +0 -924
  265. package/dist/types.js +0 -151
  266. package/dist/utils/deepmerge.d.ts +0 -9
  267. package/dist/utils/deepmerge.js +0 -61
package/README.md CHANGED
@@ -1,1101 +1,111 @@
1
1
  # ioredis-toolkit
2
2
 
3
- Production-grade, type-safe Redis infrastructure for distributed systems: a unified client, cache, rate limiter, distributed lock, pub/sub, health checking, and session management — all working in **standalone**, **sentinel**, and **cluster** modes.
3
+ Production-oriented Redis infrastructure for Node.js 22+ and TypeScript, built around one shared Redis connection boundary.
4
4
 
5
- <a name="installation"></a>
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
- | Option | Type | Default | Description |
47
- |---|---|---|---|
48
- | `mode` | `'standalone' \| 'sentinel' \| 'cluster'` | `'standalone'` | Redis topology |
49
- | `host` | `string` | `'localhost'` | Standalone host |
50
- | `port` | `number` | `6379` | Standalone port |
51
- | `url` | `string` | | Full Redis URL (e.g. `redis://:pass@host:6379/0`) |
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
- Configurations are validated with Zod (`RedisConfigSchema`). See [mode-specific configs](#mode-configs) below.
14
+ ## One client, all configuration
69
15
 
70
- ### Mode-Specific Configurations
16
+ Every module can be configured in the same `createRedisClient()` call. Module configuration is optional and normalized independently.
71
17
 
72
- #### Standalone
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
- const client = new RedisClientWrapper({
21
+ import { createRedisClient } from 'ioredis-toolkit';
22
+
23
+ const redis = createRedisClient({
76
24
  mode: 'standalone',
77
- host: 'localhost',
25
+ host: '127.0.0.1',
78
26
  port: 6379,
79
- password: 'secret',
80
- database: 0,
81
- });
82
27
 
83
- // Or with a URL:
84
- const client = new RedisClientWrapper({ mode: 'standalone', url: 'redis://:secret@localhost:6379/0' });
85
- ```
86
-
87
- #### Sentinel
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
- | Param | Type | Default | Description |
474
- |---|---|---|---|
475
- | `client` | `RedisClientWrapper` | — | The underlying Redis client |
476
- | `logger` | `LoggerLike` | `defaultLogger` | Optional pino-compatible logger |
477
- | `options.ttl` | `number` | `30000` | Lock TTL in milliseconds |
478
- | `options.retryCount` | `number` | `3` | Number of acquisition attempts |
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
- ### RedisRevocationStore Methods
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
- #### RevocationRecord type
46
+ Each module can inherit the global configuration and override selected values:
883
47
 
884
48
  ```ts
885
- type RevocationRecord = {
886
- jti: string; // The JWT jti (base64url-encoded SHA-256 of the token)
887
- expiresAt: number; // Unix seconds after which this entry may be garbage collected
888
- reason?: string; // e.g. 'logout', 'logout-all', 'password-change'
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
- #### RevocationBatchError type
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
- // EVAL: the first numKeys arguments are KEYS, everything else is ARGV.
905
- const script = `
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
- ### Key Rules
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
- ### RedisError
67
+ ## Documentation
929
68
 
930
- ```ts
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
- ### Common Error Codes
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
- | Code | When It Occurs |
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
- <a name="logging"></a>
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
- ```ts
966
- import { createLogger } from 'pino';
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
- <a name="mode-compatibility"></a>
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
- npm install # install dependencies
991
- npm run build # tsc + asset copy (Lua scripts land in dist/session/scripts)
992
- npm run typecheck # src + test + scripts (tsconfig.test.json)
993
- npm test # vitest
994
- npm run test:watch # vitest watch mode
995
- npm run format # prettier --write 'src/**/*.ts'
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
- The test suite covers the client, cache and rate limiter (including cluster-mode behavior) using in-memory fakes — no Redis server required. The session suites are gated: they run against real Redis (`localhost:6379`, or `REDIS_MODE=cluster` / `REDIS_MODE=sentinel` with the compose topologies in `test/infra/`) and skip cleanly when it is unreachable.
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
- | Type | Description |
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
- <a name="changelog"></a>
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
- See [CHANGELOG.md](CHANGELOG.md) for recent changes.
107
+ ## Links
1099
108
 
1100
- ---
1101
- *Generated with ioredis-toolkit v0.0.4*
109
+ - [Changelog](./CHANGELOG.md)
110
+ - [Issues](https://github.com/org-utils/ioredis-toolkit/issues)
111
+ - [Repository](https://github.com/org-utils/ioredis-toolkit)