@venizia/ignis-docs 0.0.8-3 → 0.1.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 (213) hide show
  1. package/README.md +7 -7
  2. package/{wiki → content}/best-practices/api-usage-examples.md +15 -12
  3. package/{wiki → content}/best-practices/architectural-patterns.md +70 -78
  4. package/{wiki → content}/best-practices/architecture-decisions.md +91 -60
  5. package/{wiki → content}/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/{wiki → content}/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/{wiki → content}/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/{wiki → content}/best-practices/code-style-standards/documentation.md +13 -13
  9. package/{wiki → content}/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/{wiki → content}/best-practices/code-style-standards/index.md +1 -1
  11. package/{wiki → content}/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/{wiki → content}/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/{wiki → content}/best-practices/code-style-standards/tooling.md +8 -5
  14. package/{wiki → content}/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/{wiki → content}/best-practices/common-pitfalls.md +56 -37
  16. package/{wiki → content}/best-practices/contribution-workflow.md +13 -14
  17. package/{wiki → content}/best-practices/data-modeling.md +44 -20
  18. package/{wiki → content}/best-practices/deployment-strategies.md +28 -27
  19. package/{wiki → content}/best-practices/error-handling.md +48 -24
  20. package/{wiki → content}/best-practices/index.md +5 -5
  21. package/{wiki → content}/best-practices/performance-optimization.md +36 -28
  22. package/{wiki → content}/best-practices/security-guidelines.md +52 -23
  23. package/{wiki → content}/best-practices/testing-strategies.md +65 -51
  24. package/{wiki → content}/best-practices/troubleshooting-tips.md +24 -24
  25. package/{wiki/extensions/components/swagger.md → content/extensions/components/api-reference.md} +40 -31
  26. package/{wiki → content}/extensions/components/authentication/api.md +19 -19
  27. package/{wiki → content}/extensions/components/authentication/errors.md +7 -7
  28. package/{wiki → content}/extensions/components/authentication/index.md +10 -8
  29. package/{wiki → content}/extensions/components/authentication/usage.md +101 -6
  30. package/{wiki → content}/extensions/components/authorization/api.md +45 -25
  31. package/{wiki → content}/extensions/components/authorization/errors.md +6 -6
  32. package/{wiki → content}/extensions/components/authorization/index.md +11 -10
  33. package/{wiki → content}/extensions/components/authorization/usage.md +21 -21
  34. package/{wiki → content}/extensions/components/health-check.md +1 -1
  35. package/{wiki → content}/extensions/components/index.md +5 -5
  36. package/{wiki → content}/extensions/components/mail/errors.md +15 -15
  37. package/{wiki → content}/extensions/components/mail/index.md +1 -2
  38. package/{wiki → content}/extensions/components/mail/usage.md +1 -1
  39. package/{wiki → content}/extensions/components/request-tracker.md +1 -1
  40. package/{wiki → content}/extensions/components/socket-io/api.md +9 -9
  41. package/{wiki → content}/extensions/components/socket-io/errors.md +5 -5
  42. package/{wiki → content}/extensions/components/socket-io/index.md +8 -8
  43. package/{wiki → content}/extensions/components/socket-io/usage.md +1 -1
  44. package/{wiki → content}/extensions/components/static-asset/api.md +17 -4
  45. package/{wiki → content}/extensions/components/static-asset/errors.md +4 -4
  46. package/{wiki → content}/extensions/components/static-asset/index.md +26 -28
  47. package/{wiki → content}/extensions/components/static-asset/usage.md +13 -12
  48. package/{wiki → content}/extensions/components/template/index.md +2 -2
  49. package/{wiki → content}/extensions/components/template/setup-page.md +1 -1
  50. package/{wiki → content}/extensions/components/websocket/api.md +3 -3
  51. package/{wiki → content}/extensions/components/websocket/errors.md +5 -5
  52. package/{wiki → content}/extensions/components/websocket/index.md +5 -5
  53. package/{wiki → content}/extensions/components/websocket/usage.md +3 -3
  54. package/{wiki → content}/extensions/helpers/cron/index.md +2 -2
  55. package/{wiki → content}/extensions/helpers/crypto/index.md +1 -1
  56. package/{wiki → content}/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +283 -0
  58. package/{wiki → content}/extensions/helpers/index.md +2 -3
  59. package/{wiki → content}/extensions/helpers/inversion/index.md +15 -7
  60. package/{wiki → content}/extensions/helpers/kafka/examples.md +1 -1
  61. package/{wiki → content}/extensions/helpers/logger/index.md +32 -2
  62. package/{wiki → content}/extensions/helpers/network/index.md +6 -0
  63. package/{wiki → content}/extensions/helpers/queue/index.md +14 -17
  64. package/content/extensions/helpers/redis/index.md +713 -0
  65. package/{wiki → content}/extensions/helpers/socket-io/index.md +14 -10
  66. package/{wiki → content}/extensions/helpers/storage/api.md +44 -8
  67. package/{wiki → content}/extensions/helpers/storage/index.md +43 -7
  68. package/{wiki → content}/extensions/helpers/template/index.md +6 -3
  69. package/{wiki → content}/extensions/helpers/types/index.md +11 -8
  70. package/{wiki → content}/extensions/helpers/websocket/api.md +9 -9
  71. package/{wiki → content}/extensions/helpers/websocket/index.md +7 -7
  72. package/{wiki → content}/extensions/helpers/worker-thread/index.md +2 -2
  73. package/{wiki → content}/extensions/index.md +3 -4
  74. package/{wiki → content}/extensions/src-details/mcp-server.md +18 -24
  75. package/{wiki → content}/guides/core-concepts/application/bootstrapping.md +11 -14
  76. package/{wiki → content}/guides/core-concepts/application/index.md +3 -3
  77. package/{wiki → content}/guides/core-concepts/components.md +19 -10
  78. package/{wiki → content}/guides/core-concepts/dependency-injection.md +6 -3
  79. package/{wiki → content}/guides/core-concepts/grpc-controllers.md +6 -5
  80. package/{wiki → content}/guides/core-concepts/persistent/datasources.md +33 -27
  81. package/{wiki → content}/guides/core-concepts/persistent/index.md +16 -5
  82. package/{wiki → content}/guides/core-concepts/persistent/models.md +24 -20
  83. package/content/guides/core-concepts/persistent/postgres-drivers.md +167 -0
  84. package/{wiki → content}/guides/core-concepts/persistent/repositories.md +40 -23
  85. package/content/guides/core-concepts/persistent/search-meilisearch.md +183 -0
  86. package/content/guides/core-concepts/persistent/search-typesense.md +429 -0
  87. package/{wiki → content}/guides/core-concepts/persistent/transactions.md +61 -25
  88. package/{wiki → content}/guides/core-concepts/rest-controllers.md +12 -9
  89. package/content/guides/core-concepts/services.md +389 -0
  90. package/{wiki → content}/guides/get-started/5-minute-quickstart.md +19 -19
  91. package/{wiki → content}/guides/get-started/philosophy.md +36 -36
  92. package/{wiki → content}/guides/get-started/setup.md +3 -3
  93. package/{wiki → content}/guides/index.md +3 -3
  94. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  95. package/{wiki → content}/guides/migrations/scoped-rbac-migration.md +17 -17
  96. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  97. package/{wiki → content}/guides/reference/glossary.md +19 -12
  98. package/{wiki → content}/guides/reference/mcp-docs-server.md +22 -18
  99. package/{wiki → content}/guides/tutorials/building-a-crud-api.md +30 -33
  100. package/{wiki → content}/guides/tutorials/complete-installation.md +17 -17
  101. package/{wiki → content}/guides/tutorials/ecommerce-api.md +158 -119
  102. package/{wiki → content}/guides/tutorials/realtime-chat.md +176 -130
  103. package/content/guides/tutorials/testing.md +264 -0
  104. package/content/index.md +5 -0
  105. package/content/public/apple-touch-icon.png +0 -0
  106. package/content/public/og-image.png +0 -0
  107. package/content/public/site.webmanifest +11 -0
  108. package/{wiki → content}/references/base/application.md +4 -5
  109. package/{wiki → content}/references/base/bootstrapping.md +18 -5
  110. package/{wiki → content}/references/base/components.md +149 -120
  111. package/content/references/base/connectors.md +178 -0
  112. package/{wiki → content}/references/base/controllers.md +41 -30
  113. package/content/references/base/datasources.md +527 -0
  114. package/{wiki → content}/references/base/dependency-injection.md +34 -22
  115. package/{wiki → content}/references/base/filter-system/application-usage.md +17 -14
  116. package/{wiki → content}/references/base/filter-system/array-operators.md +7 -2
  117. package/{wiki → content}/references/base/filter-system/comparison-operators.md +3 -0
  118. package/{wiki → content}/references/base/filter-system/default-filter.md +89 -71
  119. package/{wiki → content}/references/base/filter-system/fields-order-pagination.md +22 -22
  120. package/{wiki → content}/references/base/filter-system/index.md +6 -3
  121. package/{wiki → content}/references/base/filter-system/json-filtering.md +20 -1
  122. package/{wiki → content}/references/base/filter-system/list-operators.md +1 -1
  123. package/{wiki → content}/references/base/filter-system/logical-operators.md +33 -1
  124. package/{wiki → content}/references/base/filter-system/null-operators.md +30 -1
  125. package/{wiki → content}/references/base/filter-system/quick-reference.md +23 -4
  126. package/{wiki → content}/references/base/filter-system/tips.md +5 -5
  127. package/{wiki → content}/references/base/filter-system/use-cases.md +12 -12
  128. package/{wiki → content}/references/base/grpc-controllers.md +13 -13
  129. package/{wiki → content}/references/base/index.md +24 -12
  130. package/{wiki/references/base/middleware.md → content/references/base/middlewares.md} +205 -24
  131. package/{wiki → content}/references/base/models.md +63 -49
  132. package/{wiki → content}/references/base/providers.md +136 -130
  133. package/{wiki → content}/references/base/repositories/advanced.md +59 -58
  134. package/{wiki → content}/references/base/repositories/index.md +115 -91
  135. package/content/references/base/repositories/mixins.md +99 -0
  136. package/{wiki → content}/references/base/repositories/relations.md +54 -64
  137. package/{wiki → content}/references/base/repositories/soft-deletable.md +31 -30
  138. package/content/references/base/services.md +404 -0
  139. package/{wiki → content}/references/configuration/environment-variables.md +46 -30
  140. package/{wiki → content}/references/configuration/index.md +6 -6
  141. package/{wiki → content}/references/index.md +17 -12
  142. package/{wiki → content}/references/quick-reference.md +65 -106
  143. package/content/references/utilities/crypto.md +98 -0
  144. package/{wiki → content}/references/utilities/index.md +3 -3
  145. package/{wiki → content}/references/utilities/jsx.md +6 -4
  146. package/content/references/utilities/module.md +90 -0
  147. package/{wiki → content}/references/utilities/parse.md +4 -14
  148. package/{wiki → content}/references/utilities/promise.md +9 -7
  149. package/{wiki → content}/references/utilities/schema.md +5 -3
  150. package/dist/mcp-server/common/guards.d.ts +8 -0
  151. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  152. package/dist/mcp-server/common/guards.js +14 -0
  153. package/dist/mcp-server/common/guards.js.map +1 -0
  154. package/dist/mcp-server/common/index.d.ts +1 -0
  155. package/dist/mcp-server/common/index.d.ts.map +1 -1
  156. package/dist/mcp-server/common/index.js +1 -0
  157. package/dist/mcp-server/common/index.js.map +1 -1
  158. package/dist/mcp-server/common/paths.d.ts.map +1 -1
  159. package/dist/mcp-server/common/paths.js +2 -2
  160. package/dist/mcp-server/common/paths.js.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  162. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  163. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  164. package/dist/mcp-server/helpers/github.helper.js +1 -1
  165. package/dist/mcp-server/index.js +7 -2
  166. package/dist/mcp-server/index.js.map +1 -1
  167. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  168. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  169. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  170. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  175. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  178. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  179. package/package.json +12 -12
  180. package/wiki/extensions/helpers/error/index.md +0 -227
  181. package/wiki/extensions/helpers/redis/index.md +0 -488
  182. package/wiki/extensions/helpers/testing/index.md +0 -510
  183. package/wiki/guides/core-concepts/services.md +0 -119
  184. package/wiki/guides/tutorials/testing.md +0 -722
  185. package/wiki/index.md +0 -183
  186. package/wiki/references/base/datasources.md +0 -454
  187. package/wiki/references/base/middlewares.md +0 -590
  188. package/wiki/references/base/repositories/mixins.md +0 -335
  189. package/wiki/references/base/services.md +0 -201
  190. package/wiki/references/utilities/crypto.md +0 -56
  191. package/wiki/references/utilities/module.md +0 -42
  192. /package/{wiki → content}/extensions/components/mail/api.md +0 -0
  193. /package/{wiki → content}/extensions/components/template/api-page.md +0 -0
  194. /package/{wiki → content}/extensions/components/template/errors-page.md +0 -0
  195. /package/{wiki → content}/extensions/components/template/single-page.md +0 -0
  196. /package/{wiki → content}/extensions/components/template/usage-page.md +0 -0
  197. /package/{wiki → content}/extensions/helpers/kafka/admin.md +0 -0
  198. /package/{wiki → content}/extensions/helpers/kafka/consumer.md +0 -0
  199. /package/{wiki → content}/extensions/helpers/kafka/index.md +0 -0
  200. /package/{wiki → content}/extensions/helpers/kafka/producer.md +0 -0
  201. /package/{wiki → content}/extensions/helpers/kafka/schema-registry.md +0 -0
  202. /package/{wiki → content}/extensions/helpers/network/api.md +0 -0
  203. /package/{wiki → content}/extensions/helpers/socket-io/api.md +0 -0
  204. /package/{wiki → content}/extensions/helpers/template/single-page.md +0 -0
  205. /package/{wiki → content}/extensions/helpers/uid/index.md +0 -0
  206. /package/{wiki → content}/guides/core-concepts/components-guide.md +0 -0
  207. /package/{wiki → content}/public/logo.svg +0 -0
  208. /package/{wiki → content}/references/base/filter-system/pattern-matching.md +0 -0
  209. /package/{wiki → content}/references/base/filter-system/range-operators.md +0 -0
  210. /package/{wiki → content}/references/utilities/date.md +0 -0
  211. /package/{wiki → content}/references/utilities/performance.md +0 -0
  212. /package/{wiki → content}/references/utilities/request.md +0 -0
  213. /package/{wiki → content}/references/utilities/statuses.md +0 -0
@@ -0,0 +1,713 @@
1
+ # Redis
2
+
3
+ The Redis helper module provides a unified data API across three deployment topologies - single node, cluster, and Sentinel - via the `AbstractRedisHelper` base class. All three implement the same `IRedisHelper` interface, so a consumer written against `IRedisHelper` works with any topology and can be switched without changing application code.
4
+
5
+ Backed by **ioredis** under the hood.
6
+
7
+ ## 1. Overview
8
+
9
+ ### When to use which topology
10
+
11
+ | Topology | Class | Use when |
12
+ |----------|-------|----------|
13
+ | **Single** | `RedisSingleHelper` | One Redis node - development, simple production setups |
14
+ | **Cluster** | `RedisClusterHelper` | Sharded data across multiple nodes - horizontal scale or large datasets |
15
+ | **Sentinel** | `RedisSentinelHelper` | One logical master + replicas with automatic failover - high availability |
16
+
17
+ All three share the full data API defined in `IRedisHelper` (`get`, `set`, `hSet`, `publish`, etc.). Only the constructor options differ.
18
+
19
+ ## 2. Class and Interface Model
20
+
21
+ ### Class hierarchy
22
+
23
+ | Class | Extends | Notes |
24
+ |-------|---------|-------|
25
+ | `AbstractRedisHelper` | `BaseHelper` | Base class; implements `IRedisHelper`; never constructed directly |
26
+ | `RedisSingleHelper` | `AbstractRedisHelper` | ioredis `Redis` client; returns `Redis` from `getClient()` |
27
+ | `RedisClusterHelper` | `AbstractRedisHelper` | ioredis `Cluster` client; returns `Cluster` from `getClient()` |
28
+ | `RedisSentinelHelper` | `AbstractRedisHelper` | ioredis `Redis` client (Sentinel mode); returns `Redis` from `getClient()` |
29
+
30
+ > **Typing rule:** Declare parameters and bindings as `IRedisHelper`. Use `instanceof AbstractRedisHelper` for runtime topology checks. Never type-check against the concrete subclasses unless you need topology-specific behavior.
31
+
32
+ ### TRedisClient
33
+
34
+ ```typescript
35
+ type TRedisClient = Redis | Cluster; // ioredis
36
+ ```
37
+
38
+ `RedisSingleHelper` and `RedisSentinelHelper` return a `Redis` instance. `RedisClusterHelper` returns a `Cluster` instance. Both satisfy `TRedisClient`.
39
+
40
+ ### IRedisHelper and its nine capability interfaces
41
+
42
+ `IRedisHelper` extends all nine of the following:
43
+
44
+ | Interface | Responsibility |
45
+ |-----------|----------------|
46
+ | `IRedisConnection` | Connection lifecycle - `connect`, `disconnect`, `ping`, `getClient`, `duplicateClient` |
47
+ | `IRedisKey` | Key TTL and counters - `exists`, `expire`, `expireAt`, `ttl`, `persist`, `incr`/`decr` variants |
48
+ | `IRedisKeyValue` | String key-value - `get`, `set`, `del`, `keys`, `getString(s)`, `getObject(s)`, `mSet`, `mGet` |
49
+ | `IRedisHash` | Hash maps - `hSet`, `hGetAll`, `hGet`, `hDel`, `hExists`, `hKeys`, `hVals`, `hIncrBy`, `hLen` |
50
+ | `IRedisSet` | Sets - `sAdd`, `sRem`, `sMembers`, `sIsMember`, `sCard` |
51
+ | `IRedisList` | Lists - `lPush`, `rPush`, `lPop`, `rPop`, `lRange`, `lLen` |
52
+ | `IRedisPubSub` | Pub/Sub with optional zlib compression - `publish`, `subscribe`, `unsubscribe` |
53
+ | `IRedisJson` | RedisJSON module operations - `jSet`, `jGet`, `jDelete`, `jNumberIncreaseBy`, `jStringAppend`, `jPush`, `jPop` |
54
+ | `IRedisCommand` | Raw escape hatch - `execute(command, params?)` |
55
+
56
+ ## 3. Construction - Single
57
+
58
+ ```typescript
59
+ import { RedisSingleHelper } from '@venizia/ignis-helpers';
60
+
61
+ const redis = new RedisSingleHelper({
62
+ name: 'cache',
63
+ host: 'localhost',
64
+ port: 6379,
65
+ password: 'secret',
66
+ database: 0,
67
+ autoConnect: true,
68
+ maxRetry: 5,
69
+
70
+ onInitialized: ({ name, helper }) => {
71
+ console.log(`[${name}] initialized`);
72
+ },
73
+ onConnected: ({ name }) => {
74
+ console.log(`[${name}] connected`);
75
+ },
76
+ onReady: ({ name }) => {
77
+ console.log(`[${name}] ready`);
78
+ },
79
+ onError: ({ name, error }) => {
80
+ console.error(`[${name}] error`, error);
81
+ },
82
+ });
83
+ ```
84
+
85
+ ### IRedisSingleHelperOptions
86
+
87
+ Combines `IRedisSingleHelperProps` and `IRedisHelperCallbacks`.
88
+
89
+ | Field | Type | Required | Default | Description |
90
+ |-------|------|----------|---------|-------------|
91
+ | `name` | `string` | Yes | - | Helper identifier used in logs and as `IRedisHelper.name` |
92
+ | `host` | `string` | Yes | - | Redis server hostname |
93
+ | `port` | `string \| number` | Yes | - | Redis server port |
94
+ | `password` | `string` | Yes | - | Redis `requirepass` value |
95
+ | `user` | `string` | No | - | Redis username (ACL authentication) |
96
+ | `database` | `number` | No | `0` | Redis database index (0-15) |
97
+ | `autoConnect` | `boolean` | No | `true` | Connect immediately; `false` uses ioredis `lazyConnect` |
98
+ | `maxRetry` | `number` | No | `0` | Max reconnect attempts; `0` = unlimited; `-1` = no retry |
99
+ | `onInitialized` | `(opts: { name: string; helper: IRedisHelper }) => void` | No | - | Fired synchronously after construction |
100
+ | `onConnected` | `(opts: { name: string; helper: IRedisHelper }) => void` | No | - | Fired on TCP connection established |
101
+ | `onReady` | `(opts: { name: string; helper: IRedisHelper }) => void` | No | - | Fired when client is ready for commands |
102
+ | `onError` | `(opts: { name: string; helper: IRedisHelper; error: unknown }) => void` | No | - | Fired on connection or command errors |
103
+
104
+ ### Retry strategy
105
+
106
+ Backoff formula: `Math.max(Math.min(attempt * 2000, 5000), 1000)` - starting at 1 s, capped at 5 s. Reconnect stops when `attempt > maxRetry` (if `maxRetry > -1`). The framework always sets `maxRetriesPerRequest: null` internally, which is required for BullMQ compatibility.
107
+
108
+ ## 4. Construction - Cluster
109
+
110
+ ```typescript
111
+ import { RedisClusterHelper } from '@venizia/ignis-helpers';
112
+
113
+ const cluster = new RedisClusterHelper({
114
+ name: 'cache-cluster',
115
+ nodes: [
116
+ { host: 'redis-node-1', port: 7000 },
117
+ { host: 'redis-node-2', port: 7001 },
118
+ { host: 'redis-node-3', port: 7002 },
119
+ ],
120
+ clusterOptions: {
121
+ redisOptions: { password: 'cluster-password' },
122
+ },
123
+ onReady: ({ name }) => {
124
+ console.log(`[${name}] cluster ready`);
125
+ },
126
+ });
127
+ ```
128
+
129
+ ### IRedisClusterHelperOptions
130
+
131
+ | Field | Type | Required | Default | Description |
132
+ |-------|------|----------|---------|-------------|
133
+ | `name` | `string` | Yes | - | Helper identifier |
134
+ | `nodes` | `Array<{ host: string; port: string \| number; password?: string }>` | Yes | - | Startup nodes - ioredis discovers the rest |
135
+ | `clusterOptions` | `ClusterOptions` | No | - | Passed verbatim to `new Cluster(nodes, clusterOptions)` |
136
+ | `onInitialized` / `onConnected` / `onReady` / `onError` | callbacks | No | - | Same shape as single |
137
+
138
+ > **No framework defaults injected.** Cluster does not apply the backoff retry strategy or `maxRetriesPerRequest: null` automatically. Pass those inside `clusterOptions.redisOptions` if your consumers (e.g. BullMQ) require them.
139
+
140
+ `duplicateClient()` on a cluster creates a **new** `Cluster` instance from the same startup nodes and options (because ioredis `Cluster` does not implement `.duplicate()`).
141
+
142
+ ## 5. Construction - Sentinel
143
+
144
+ ### What Redis Sentinel is
145
+
146
+ Redis Sentinel is a high-availability (HA) architecture made of three process types:
147
+
148
+ ```
149
+ +------------------+ monitors +--------------------+
150
+ | Sentinel (1) |<------------------>| Master (primary) |
151
+ +------------------+ +--------------------+
152
+ +------------------+ replication |
153
+ | Sentinel (2) | +--------------+
154
+ +------------------+ v
155
+ +------------------+ +--------------------+
156
+ | Sentinel (3) | | Replica (standby) |
157
+ +------------------+ +--------------------+
158
+ ```
159
+
160
+ - **Sentinels** form a quorum (typically 3 processes). They monitor the master, agree by majority vote when it is down, and promote a replica to master (failover).
161
+ - **Master** accepts all writes.
162
+ - **Replicas** replicate from master and serve reads when `role: 'slave'` is set.
163
+
164
+ ### Why your app connects to sentinels, not the master
165
+
166
+ The master's address can change after a failover. Instead of hard-coding a master host, you give ioredis the sentinel addresses and the monitored master group name (`masterName`). ioredis asks a sentinel for the current master, connects to it, and re-queries sentinels automatically after a failover. **Your application code does not change during a failover** - the only observable effect is a few seconds of transient command failures, which the retry strategy handles.
167
+
168
+ ### Constructor
169
+
170
+ ```typescript
171
+ import { RedisSentinelHelper } from '@venizia/ignis-helpers';
172
+
173
+ const redis = new RedisSentinelHelper({
174
+ name: 'ha-cache', // helper identifier (logging/scope)
175
+ masterName: 'mymaster', // group name in sentinel.conf "sentinel monitor mymaster ..."
176
+ sentinels: [
177
+ { host: '10.0.0.1', port: 26379 },
178
+ { host: '10.0.0.2', port: 26379 },
179
+ { host: '10.0.0.3' }, // port defaults to 26379
180
+ ],
181
+ role: 'master', // 'master' (default) or 'slave'
182
+ password: 'data-secret', // master/replica requirepass
183
+ sentinelPassword: 'sentinel-secret', // sentinel.conf requirepass
184
+ database: 0,
185
+ autoConnect: true,
186
+ maxRetry: 5,
187
+ });
188
+ ```
189
+
190
+ ### IRedisSentinelHelperOptions
191
+
192
+ | Field | Type | Required | Default | Description |
193
+ |-------|------|----------|---------|-------------|
194
+ | `name` | `string` | Yes | - | Helper identifier (logs, `IRedisHelper.name`) |
195
+ | `masterName` | `string` | Yes | - | Sentinel-monitored master group name - maps to ioredis `name` |
196
+ | `sentinels` | `Array<{ host: string; port?: string \| number }>` | Yes | - | Sentinel process addresses |
197
+ | `role` | `TRedisSentinelRole` (`RedisSentinelRoles.MASTER \| SLAVE`) | No | `RedisSentinelRoles.MASTER` | Connect to master (writes) or a replica (reads) |
198
+ | `password` | `string` | No | - | Data-node `requirepass` (master and replicas) |
199
+ | `sentinelPassword` | `string` | No | - | Sentinel-process `requirepass` |
200
+ | `sentinelUsername` | `string` | No | - | Sentinel ACL username |
201
+ | `database` | `number` | No | `0` | Redis database index |
202
+ | `autoConnect` | `boolean` | No | `true` | Connect immediately |
203
+ | `maxRetry` | `number` | No | `0` | Max reconnect attempts |
204
+ | `redisOptions` | `Partial<RedisOptions>` | No | - | Extra ioredis options; first-class fields above always override matching keys here |
205
+ | `onInitialized` / `onConnected` / `onReady` / `onError` | callbacks | No | - | Same shape as single |
206
+
207
+ ### Field clarity - name vs masterName
208
+
209
+ This is the most common source of confusion:
210
+
211
+ | Field | What it is | Where it goes |
212
+ |-------|-----------|---------------|
213
+ | `name` | The helper's own identifier used for logging and `IRedisHelper.name` | `AbstractRedisHelper` identifier |
214
+ | `masterName` | The master group being monitored by Sentinel processes, matching `sentinel monitor <name> ...` in sentinel.conf | Maps to ioredis `name` option |
215
+
216
+ They are independent. You can name the helper `'ha-cache'` while `masterName` is `'mymaster'`.
217
+
218
+ ### The authentication model - 4 relationships
219
+
220
+ Sentinel deployments have four separate authentication relationships. The app only configures two of them; the other two are server-side.
221
+
222
+ | # | From | To | App field | Server config |
223
+ |---|------|----|-----------|---------------|
224
+ | 1 | **App** | **Sentinel processes** | `sentinelPassword` | `requirepass` in sentinel.conf |
225
+ | 2 | **App** | **Data nodes** (master/replica) | `password` | `requirepass` in redis.conf |
226
+ | 3 | **Sentinel** | **Data nodes** | (not set by app) | `sentinel auth-pass <group> <pass>` in sentinel.conf |
227
+ | 4 | **Replica** | **Master** (replication) | (not set by app) | `masterauth` in redis.conf |
228
+
229
+ **Relationship 3 is the silent failure.** If the data nodes require a password but sentinel.conf does not have `sentinel auth-pass`, the sentinel processes cannot check master health and will trigger false failovers. Configure it on the server side even when your app only sets `password`.
230
+
231
+ **Common cases:**
232
+ - Private network, sentinels unauthenticated: set only `password`.
233
+ - Fully secured cluster: set both `password` and `sentinelPassword`; configure relationships 3 and 4 on the servers.
234
+
235
+ ### Failover behavior
236
+
237
+ When the master fails:
238
+
239
+ 1. Sentinels reach quorum and promote a replica.
240
+ 2. ioredis detects the master change via sentinel notification.
241
+ 3. ioredis reconnects to the new master transparently.
242
+ 4. Any in-flight commands during the few-second window may fail transiently - the retry strategy retries them.
243
+ 5. **Your application code does not change.** The same `RedisSentinelHelper` instance continues to work after failover.
244
+
245
+ ### Local testing with Docker Compose
246
+
247
+ The following Compose file starts a master, one replica, and one sentinel with full authentication. It demonstrates all four relationships.
248
+
249
+ ```yaml
250
+ version: '3.8'
251
+
252
+ services:
253
+ redis-master:
254
+ image: redis:7-alpine
255
+ command: redis-server --requirepass data-secret --masterauth data-secret
256
+ ports:
257
+ - '6379:6379'
258
+
259
+ redis-replica:
260
+ image: redis:7-alpine
261
+ command: >
262
+ redis-server
263
+ --requirepass data-secret
264
+ --masterauth data-secret
265
+ --replicaof redis-master 6379
266
+ depends_on:
267
+ - redis-master
268
+
269
+ redis-sentinel:
270
+ image: redis:7-alpine
271
+ command: >
272
+ sh -c "
273
+ echo 'sentinel monitor mymaster redis-master 6379 1' > /sentinel.conf &&
274
+ echo 'sentinel auth-pass mymaster data-secret' >> /sentinel.conf &&
275
+ echo 'sentinel down-after-milliseconds mymaster 3000' >> /sentinel.conf &&
276
+ echo 'sentinel failover-timeout mymaster 10000' >> /sentinel.conf &&
277
+ echo 'requirepass sentinel-secret' >> /sentinel.conf &&
278
+ redis-sentinel /sentinel.conf
279
+ "
280
+ ports:
281
+ - '26379:26379'
282
+ depends_on:
283
+ - redis-master
284
+ - redis-replica
285
+ ```
286
+
287
+ Connect and verify failover:
288
+
289
+ ```typescript
290
+ import { RedisSentinelHelper } from '@venizia/ignis-helpers';
291
+
292
+ const redis = new RedisSentinelHelper({
293
+ name: 'test',
294
+ masterName: 'mymaster',
295
+ sentinels: [{ host: 'localhost', port: 26379 }],
296
+ password: 'data-secret',
297
+ sentinelPassword: 'sentinel-secret',
298
+ onReady: ({ name }) => console.log(`[${name}] ready`),
299
+ });
300
+
301
+ await redis.set({ key: 'hello', value: 'world' });
302
+ const val = await redis.get({ key: 'hello' });
303
+ console.log(val); // "world"
304
+
305
+ // Stop the master container - wait ~5 s - the replica is promoted.
306
+ // The same redis instance will reconnect automatically and continue working.
307
+ ```
308
+
309
+ ## 6. Selecting a Topology - the Factory
310
+
311
+ `createRedisHelper` is an overloaded factory that returns the concrete type when `mode` is a literal.
312
+
313
+ ```typescript
314
+ import { createRedisHelper, RedisModes } from '@venizia/ignis-helpers';
315
+
316
+ // Type inferred as RedisSingleHelper
317
+ const single = createRedisHelper({
318
+ mode: RedisModes.SINGLE,
319
+ name: 'cache',
320
+ host: process.env.REDIS_HOST!,
321
+ port: Number(process.env.REDIS_PORT),
322
+ password: process.env.REDIS_PASSWORD!,
323
+ });
324
+
325
+ // Type inferred as RedisSentinelHelper
326
+ const sentinel = createRedisHelper({
327
+ mode: RedisModes.SENTINEL,
328
+ name: 'cache-ha',
329
+ masterName: 'mymaster',
330
+ sentinels: [{ host: process.env.SENTINEL_HOST! }],
331
+ password: process.env.REDIS_PASSWORD!,
332
+ });
333
+ ```
334
+
335
+ Switch by environment:
336
+
337
+ ```typescript
338
+ import { createRedisHelper, RedisModes, type IRedisHelper } from '@venizia/ignis-helpers';
339
+
340
+ const mode = (process.env.REDIS_MODE ?? RedisModes.SINGLE) as typeof RedisModes.SINGLE;
341
+
342
+ const redis: IRedisHelper = createRedisHelper({
343
+ mode,
344
+ name: 'app-cache',
345
+ host: process.env.REDIS_HOST!,
346
+ port: Number(process.env.REDIS_PORT),
347
+ password: process.env.REDIS_PASSWORD!,
348
+ });
349
+ ```
350
+
351
+ | `mode` value | Returns |
352
+ |---|---|
353
+ | `RedisModes.SINGLE` (`'single'`) | `RedisSingleHelper` |
354
+ | `RedisModes.CLUSTER` (`'cluster'`) | `RedisClusterHelper` |
355
+ | `RedisModes.SENTINEL` (`'sentinel'`) | `RedisSentinelHelper` |
356
+
357
+ Unknown mode throws an `ApplicationError`.
358
+
359
+ ## 7. Lifecycle and Events
360
+
361
+ ### autoConnect and manual connect
362
+
363
+ When `autoConnect: true` (default), the ioredis client starts connecting immediately in the constructor. When `autoConnect: false`, ioredis uses `lazyConnect` mode and you must call `connect()` explicitly before issuing commands.
364
+
365
+ ```typescript
366
+ // Lazy connect
367
+ const redis = new RedisSingleHelper({
368
+ name: 'cache',
369
+ host: 'localhost',
370
+ port: 6379,
371
+ password: 'secret',
372
+ autoConnect: false,
373
+ });
374
+
375
+ await redis.connect(); // resolves true when status === 'ready'
376
+ ```
377
+
378
+ ### Connection methods
379
+
380
+ | Method | Behavior |
381
+ |--------|----------|
382
+ | `connect()` | Resolves `false` (no-op) if status is `ready`, `reconnecting`, or `connecting`; otherwise calls ioredis `connect()` and resolves `true` when status reaches `ready` |
383
+ | `disconnect()` | Resolves `false` (no-op) if status is `end` or `close`; otherwise sends `QUIT` and resolves `true` on `'OK'` |
384
+ | `ping()` | Sends `PING`; returns `'PONG'` |
385
+
386
+ ### The four lifecycle callbacks
387
+
388
+ All helpers accept `IRedisHelperCallbacks`:
389
+
390
+ | Callback | Fired when | ioredis event |
391
+ |----------|-----------|---------------|
392
+ | `onInitialized` | Synchronously at the end of the constructor | - |
393
+ | `onConnected` | TCP connection is established | `connect` |
394
+ | `onReady` | Client is ready to accept commands | `ready` |
395
+ | `onError` | A connection or command error occurs | `error` |
396
+
397
+ The client also logs a `WARN` internally on `reconnecting` events (not surfaced as a callback).
398
+
399
+ ### duplicateClient
400
+
401
+ ```typescript
402
+ const dedicated = redis.duplicateClient();
403
+ ```
404
+
405
+ Creates an **independent** ioredis connection from the same configuration. The duplicate starts disconnected - ioredis auto-connects it on first use. Use this whenever a consumer needs its own dedicated connection (Pub/Sub, BullMQ, Socket.IO adapters). For `RedisClusterHelper`, `duplicateClient()` constructs a new `Cluster` from the same startup nodes and options.
406
+
407
+ ## 8. Full Method Reference
408
+
409
+ ### General notes (apply to all families)
410
+
411
+ - **Empty-input no-ops:** Methods that accept an array (`keys`, `members`, `values`, `fields`, `payload`) return `0` or `[]` immediately when the array is empty, without calling ioredis. This prevents the "wrong number of arguments" error ioredis throws on empty varargs.
412
+ - **Boolean mapping:** `expire`, `expireAt`, `persist`, `hExists`, `sIsMember` map ioredis numeric replies to `true` (`=== 1`) / `false`.
413
+ - **JSON auto-serialization:** `set`, `mSet`, `jSet`, `jPush` call `JSON.stringify` before writing. `getObject`, `getObjects`, `jGet` parse on read. `get` and `mGet` return raw strings unless you pass `transform`.
414
+ - **TTL units:** `set.options.expiresIn` is **milliseconds** (ioredis `PX`). `expire.seconds` is **seconds** (ioredis `EXPIRE`). `expireAt.atEpochSeconds` is a Unix epoch **in seconds** (ioredis `EXPIREAT`).
415
+ - **Pub/Sub errors:** On subscription callback error, the helper **logs** the error and returns without throwing, because throwing inside an ioredis async callback would be an unhandled rejection.
416
+
417
+ ---
418
+
419
+ ### IRedisConnection - connection lifecycle
420
+
421
+ | Method | Signature | Behavior |
422
+ |--------|-----------|----------|
423
+ | `getClient` | `(): TRedisClient` | Returns the raw ioredis `Redis` or `Cluster` instance |
424
+ | `duplicateClient` | `(): TRedisClient` | Creates an independent ioredis connection (same config, separate socket) |
425
+ | `ping` | `(): Promise<string>` | Sends `PING`; returns `'PONG'` on success |
426
+ | `connect` | `(): Promise<boolean>` | Connects the client; resolves `false` if already connected/connecting/ready |
427
+ | `disconnect` | `(): Promise<boolean>` | Graceful `QUIT`; resolves `false` if already ended/closed |
428
+
429
+ ---
430
+
431
+ ### IRedisKey - key lifecycle and counters
432
+
433
+ | Method | Signature | Behavior |
434
+ |--------|-----------|----------|
435
+ | `exists` | `(opts: { keys: string[] }): Promise<number>` | Returns the count of keys that exist; empty `keys` returns `0` without calling Redis |
436
+ | `expire` | `(opts: { key: string; seconds: number }): Promise<boolean>` | Sets expiry in seconds; `true` if applied |
437
+ | `expireAt` | `(opts: { key: string; atEpochSeconds: number }): Promise<boolean>` | Sets expiry at an epoch second timestamp; `true` if applied |
438
+ | `ttl` | `(opts: { key: string }): Promise<number>` | Returns remaining TTL in seconds; `-1` = no expiry; `-2` = key missing |
439
+ | `persist` | `(opts: { key: string }): Promise<boolean>` | Removes expiry; `true` if the timeout was removed |
440
+ | `incr` | `(opts: { key: string }): Promise<number>` | Atomically increments integer at key by 1; returns new value |
441
+ | `decr` | `(opts: { key: string }): Promise<number>` | Atomically decrements integer at key by 1; returns new value |
442
+ | `incrBy` | `(opts: { key: string; value: number }): Promise<number>` | Atomically increments by `value`; returns new value |
443
+ | `decrBy` | `(opts: { key: string; value: number }): Promise<number>` | Atomically decrements by `value`; returns new value |
444
+
445
+ ---
446
+
447
+ ### IRedisKeyValue - string key-value
448
+
449
+ | Method | Signature | Behavior |
450
+ |--------|-----------|----------|
451
+ | `set<T>` | `(opts: { key: string; value: T; options?: { log?: boolean; expiresIn?: number } }): Promise<void>` | JSON-serializes `value` and writes it; `expiresIn` is in **milliseconds** (uses `PX`); `log: true` emits an info log |
452
+ | `get<T>` | `(opts: { key: string; transform?: (input: string) => T }): Promise<T \| null>` | Returns raw string or applies `transform`; `null` if key missing |
453
+ | `del` | `(opts: { keys: string[] }): Promise<number>` | Deletes one or more keys; empty `keys` returns `0` |
454
+ | `keys` | `(opts: { key: string }): Promise<string[]>` | Runs Redis `KEYS` with a glob pattern; avoid on large keyspaces - prefer `SCAN` via `execute` |
455
+ | `getString` | `(opts: { key: string }): Promise<string \| null>` | Alias for `get` without transform |
456
+ | `getStrings` | `(opts: { keys: string[] }): Promise<(string \| null)[]>` | Alias for `mGet` without transform |
457
+ | `getObject<T>` | `(opts: { key: string }): Promise<T \| null>` | `get` with `JSON.parse` transform |
458
+ | `getObjects` | `(opts: { keys: string[] }): Promise<(unknown \| null)[]>` | `mGet` with `JSON.parse` transform |
459
+ | `mSet<T>` | `(opts: { payload: Array<{ key: string; value: T }>; options?: { log?: boolean } }): Promise<void>` | Bulk write; each value is JSON-serialized; empty `payload` is a no-op |
460
+ | `mGet<T>` | `(opts: { keys: string[]; transform?: (input: string) => T }): Promise<(T \| null)[]>` | Bulk read with optional transform; empty `keys` returns `[]` |
461
+
462
+ ---
463
+
464
+ ### IRedisHash - hash maps
465
+
466
+ | Method | Signature | Behavior |
467
+ |--------|-----------|----------|
468
+ | `hSet<T>` | `(opts: { key: string; value: T; options?: { log?: boolean } }): Promise<number>` | Sets all fields in a hash object; returns number of **new** fields added |
469
+ | `hGetAll` | `(opts: { key: string; transform?: <T, R>(input: T) => R }): Promise<unknown>` | Returns all fields and values of a hash; applies optional transform |
470
+ | `hGet` | `(opts: { key: string; field: string }): Promise<string \| null>` | Returns the value of a single hash field |
471
+ | `hDel` | `(opts: { key: string; fields: string[] }): Promise<number>` | Deletes one or more hash fields; empty `fields` returns `0` |
472
+ | `hExists` | `(opts: { key: string; field: string }): Promise<boolean>` | `true` if the field exists in the hash |
473
+ | `hKeys` | `(opts: { key: string }): Promise<string[]>` | Returns all field names in the hash |
474
+ | `hVals` | `(opts: { key: string }): Promise<string[]>` | Returns all field values in the hash |
475
+ | `hIncrBy` | `(opts: { key: string; field: string; value: number }): Promise<number>` | Atomically increments a numeric hash field; returns new value |
476
+ | `hLen` | `(opts: { key: string }): Promise<number>` | Returns the number of fields in the hash |
477
+
478
+ ---
479
+
480
+ ### IRedisSet - sets
481
+
482
+ | Method | Signature | Behavior |
483
+ |--------|-----------|----------|
484
+ | `sAdd` | `(opts: { key: string; members: Array<string \| number> }): Promise<number>` | Adds members to a set; returns count of new members; empty `members` returns `0` |
485
+ | `sRem` | `(opts: { key: string; members: Array<string \| number> }): Promise<number>` | Removes members from a set; returns count removed; empty `members` returns `0` |
486
+ | `sMembers` | `(opts: { key: string }): Promise<string[]>` | Returns all members of the set |
487
+ | `sIsMember` | `(opts: { key: string; member: string \| number }): Promise<boolean>` | `true` if `member` belongs to the set |
488
+ | `sCard` | `(opts: { key: string }): Promise<number>` | Returns the number of members in the set |
489
+
490
+ ---
491
+
492
+ ### IRedisList - lists
493
+
494
+ | Method | Signature | Behavior |
495
+ |--------|-----------|----------|
496
+ | `lPush` | `(opts: { key: string; values: Array<string \| number> }): Promise<number>` | Prepends values to the list head; returns list length after push; empty `values` returns `0` |
497
+ | `rPush` | `(opts: { key: string; values: Array<string \| number> }): Promise<number>` | Appends values to the list tail; returns list length after push; empty `values` returns `0` |
498
+ | `lPop` | `(opts: { key: string }): Promise<string \| null>` | Removes and returns the head element; `null` if list is empty |
499
+ | `rPop` | `(opts: { key: string }): Promise<string \| null>` | Removes and returns the tail element; `null` if list is empty |
500
+ | `lRange` | `(opts: { key: string; start: number; stop: number }): Promise<string[]>` | Returns elements from `start` to `stop` (inclusive, 0-indexed, `-1` = last) |
501
+ | `lLen` | `(opts: { key: string }): Promise<number>` | Returns the number of elements in the list |
502
+
503
+ ---
504
+
505
+ ### IRedisPubSub - publish/subscribe
506
+
507
+ | Method | Signature | Behavior |
508
+ |--------|-----------|----------|
509
+ | `publish<T>` | `(opts: { topics: string[]; payload: T; useCompress?: boolean }): Promise<void>` | JSON-serializes `payload` into a `Buffer`; optionally zlib-deflates it; publishes to each topic; skips empty/blank topics (logs error, does not throw) |
510
+ | `subscribe` | `(opts: { topic: string }): void` | Subscribes the client to `topic`; on subscription error the helper **logs** it and does not throw |
511
+ | `unsubscribe` | `(opts: { topic: string }): void` | Unsubscribes from `topic`; on error the helper **logs** it and does not throw |
512
+
513
+ > **Important:** A subscribed ioredis connection enters subscriber mode and cannot run regular commands. Use `duplicateClient()` to obtain a separate connection for pub/sub.
514
+
515
+ ```typescript
516
+ // Correct pattern - separate connections for data and pub/sub
517
+ const dataClient = new RedisSingleHelper({ name: 'data', host, port, password });
518
+ const subClient = new RedisSingleHelper({ name: 'sub', host, port, password });
519
+
520
+ subClient.subscribe({ topic: 'events' });
521
+ subClient.getClient().on('message', (channel, message) => {
522
+ const parsed = JSON.parse(message.toString());
523
+ console.log(channel, parsed);
524
+ });
525
+
526
+ await dataClient.set({ key: 'foo', value: 'bar' });
527
+ ```
528
+
529
+ ---
530
+
531
+ ### IRedisJson - RedisJSON module
532
+
533
+ Requires the [RedisJSON module](https://redis.io/docs/stack/json/) on the server.
534
+
535
+ | Method | Signature | Behavior |
536
+ |--------|-----------|----------|
537
+ | `jSet<T>` | `(opts: { key: string; path: string; value: T }): Promise<string \| null>` | Sets a JSON document at `path` (`JSON.SET`); value is `JSON.stringify`-ed |
538
+ | `jGet<T>` | `(opts: { key: string; path?: string }): Promise<T \| null>` | Retrieves the document or sub-path (`JSON.GET`); `path` defaults to `'$'` |
539
+ | `jDelete` | `(opts: { key: string; path?: string }): Promise<number>` | Deletes the document or sub-path (`JSON.DEL`); `path` defaults to `'$'`; returns count deleted |
540
+ | `jNumberIncreaseBy` | `(opts: { key: string; path: string; value: number }): Promise<string \| null>` | Increments a numeric field by `value` (`JSON.NUMINCRBY`) |
541
+ | `jStringAppend` | `(opts: { key: string; path: string; value: string }): Promise<number[] \| null>` | Appends `value` to a string field (`JSON.STRAPPEND`); returns new string lengths |
542
+ | `jPush<T>` | `(opts: { key: string; path: string; value: T }): Promise<number[] \| null>` | Appends `value` to an array field (`JSON.ARRAPPEND`); value is `JSON.stringify`-ed; returns new array lengths |
543
+ | `jPop<T>` | `(opts: { key: string; path: string }): Promise<T \| null>` | Pops the last element from an array field (`JSON.ARRPOP`) |
544
+
545
+ ---
546
+
547
+ ### IRedisCommand - raw escape hatch
548
+
549
+ | Method | Signature | Behavior |
550
+ |--------|-----------|----------|
551
+ | `execute<R>` | `(command: string, parameters?: Array<string \| number \| Buffer>): Promise<R>` | Calls any ioredis command directly via `client.call(command, parameters)` |
552
+
553
+ ```typescript
554
+ // Use SCAN instead of KEYS on large keyspaces
555
+ const [cursor, keys] = await redis.execute<[string, string[]]>(
556
+ 'SCAN',
557
+ [0, 'MATCH', 'user:*', 'COUNT', 100],
558
+ );
559
+
560
+ // Fetch server info
561
+ const info = await redis.execute<string>('INFO');
562
+ ```
563
+
564
+ ## 9. Using the Helper Across IGNIS
565
+
566
+ All three topologies implement `IRedisHelper` and are interchangeable in every integration below. Switch topology by changing the helper you construct; the consumer code does not change.
567
+
568
+ ### BullMQ (BullMQHelper)
569
+
570
+ `BullMQHelper` accepts a `redisConnection: IRedisHelper`. Internally it:
571
+ - Calls `redisConnection.duplicateClient()` to create dedicated `Queue` and `Worker` connections (BullMQ requires separate connections per role).
572
+ - Calls `redisConnection.getClient() instanceof Cluster` to detect cluster topology and adjust BullMQ configuration.
573
+
574
+ `RedisSingleHelper` (and `RedisSentinelHelper`) are BullMQ-compatible out of the box because the framework sets `maxRetriesPerRequest: null` in `buildDefaultOpts`.
575
+
576
+ ```typescript
577
+ import { BullMQHelper } from '@venizia/ignis-helpers';
578
+ import { RedisSingleHelper } from '@venizia/ignis-helpers';
579
+
580
+ const redis = new RedisSingleHelper({ name: 'queue-redis', host, port, password });
581
+
582
+ const queue = BullMQHelper.newInstance({
583
+ queueName: 'email',
584
+ role: 'queue',
585
+ redisConnection: redis,
586
+ });
587
+ ```
588
+
589
+ ### Socket.IO component (SocketIOServerHelper)
590
+
591
+ The Socket.IO component resolves an `AbstractRedisHelper` from its Redis-connection binding key (value `@app/socket-io/redis-connection`). Internally `SocketIOServerHelper` calls `duplicateClient()` three times - once each for the pub channel, sub channel, and emitter adapter. Bind any topology (single/cluster/sentinel); the consumer code is identical:
592
+
593
+ ```typescript
594
+ import { AbstractRedisHelper, RedisSentinelHelper } from '@venizia/ignis-helpers';
595
+
596
+ // In your Application preConfigure:
597
+ const redis = new RedisSentinelHelper({ name: 'socket-redis', masterName: 'mymaster', sentinels, password });
598
+ this.bind<AbstractRedisHelper>({ key: '@app/socket-io/redis-connection' }).to(redis);
599
+ ```
600
+
601
+ See the Socket.IO component documentation for the full component registration and adapter setup.
602
+
603
+ ### WebSocket component (WebSocket server and emitter)
604
+
605
+ The WebSocket component resolves an `AbstractRedisHelper` from its Redis-connection binding key (value `@app/websocket/redis-connection`). The `WebSocketServerHelper` calls `duplicateClient()` for its pub and sub connections; the WebSocket emitter calls `duplicateClient()` for its pub connection. Bind any topology:
606
+
607
+ ```typescript
608
+ import { AbstractRedisHelper, RedisClusterHelper } from '@venizia/ignis-helpers';
609
+
610
+ const redis = new RedisClusterHelper({ name: 'ws-redis', nodes });
611
+ this.bind<AbstractRedisHelper>({ key: '@app/websocket/redis-connection' }).to(redis);
612
+ ```
613
+
614
+ See the WebSocket component documentation for the full component registration.
615
+
616
+ ### Casbin authorization enforcer
617
+
618
+ The cached Casbin enforcer (`ICasbinEnforcerCachedRedis`) accepts `connection: IRedisHelper` inside its `options`. It calls the helper's direct methods - `get`, `set` (with `options.expiresIn`), and `del` - to read, write, and invalidate cached policy keys; it does not reach the raw client. Any topology works as the `connection`:
619
+
620
+ ```typescript
621
+ import type { IRedisHelper } from '@venizia/ignis-helpers';
622
+
623
+ // Shape of the cached-enforcer config (see the Authorization component docs for full setup):
624
+ const cached = {
625
+ use: true,
626
+ driver: CasbinEnforcerCachedDrivers.REDIS,
627
+ options: {
628
+ connection: redis as IRedisHelper, // any RedisSingleHelper / RedisClusterHelper / RedisSentinelHelper
629
+ expiresIn: 300_000, // cached-policy TTL in milliseconds
630
+ keyFn: ({ user }) => `authz:policy:${user.id}`,
631
+ },
632
+ };
633
+ ```
634
+
635
+ ### Binding into an application
636
+
637
+ ```typescript
638
+ import { RedisSingleHelper } from '@venizia/ignis-helpers';
639
+ import { BaseApplication } from '@venizia/ignis';
640
+
641
+ class MyApp extends BaseApplication {
642
+ async preConfigure() {
643
+ const redis = new RedisSingleHelper({
644
+ name: 'cache',
645
+ host: process.env.REDIS_HOST!,
646
+ port: Number(process.env.REDIS_PORT),
647
+ password: process.env.REDIS_PASSWORD!,
648
+ });
649
+
650
+ // Bind so components and services can resolve it
651
+ this.bind<RedisSingleHelper>({ key: 'helpers.RedisCache' }).to(redis);
652
+ }
653
+ }
654
+ ```
655
+
656
+ ## 10. Production Notes
657
+
658
+ - **Use a singleton per logical connection.** Construct the helper once at startup and share it. Do not construct a new helper per request - each construction opens a new connection pool.
659
+
660
+ - **`duplicateClient()` for Pub/Sub.** A subscribed ioredis connection cannot run normal commands. Always call `duplicateClient()` to get a dedicated subscriber connection and keep the main client for data operations.
661
+
662
+ - **Sentinel for HA; Cluster for sharding.** These are different problems. Sentinel gives you automatic failover for a single logical master. Cluster shards data across nodes. They are not interchangeable.
663
+
664
+ - **TTL unit reminder:** `set.options.expiresIn` is **milliseconds**; `expire.seconds` is **seconds**; `expireAt.atEpochSeconds` is a Unix epoch in **seconds**. Mixing these up causes unexpected key expirations.
665
+
666
+ - **Sentinel `sentinel auth-pass` is a server-side requirement.** If your data nodes require a password, configure `sentinel auth-pass <group> <password>` in sentinel.conf in addition to the app-side `password` field. Missing it causes false failovers even when `password` is correct.
667
+
668
+ - **Avoid `keys()` in production.** The Redis `KEYS` command scans the entire keyspace and blocks the server. Use `execute('SCAN', [...])` for production pattern matching.
669
+
670
+ ## Import Reference
671
+
672
+ ```typescript
673
+ import {
674
+ // Classes
675
+ AbstractRedisHelper,
676
+ RedisSingleHelper,
677
+ RedisClusterHelper,
678
+ RedisSentinelHelper,
679
+ // Factory
680
+ createRedisHelper,
681
+ RedisModes,
682
+ // Sentinel role const-class
683
+ RedisSentinelRoles,
684
+ } from '@venizia/ignis-helpers';
685
+
686
+ import type {
687
+ // Interface
688
+ IRedisHelper,
689
+ // Option types
690
+ IRedisSingleHelperOptions,
691
+ IRedisSingleHelperProps,
692
+ IRedisClusterHelperOptions,
693
+ IRedisClusterHelperProps,
694
+ IRedisSentinelHelperOptions,
695
+ IRedisSentinelHelperProps,
696
+ IRedisHelperCallbacks,
697
+ // Client + enum types
698
+ TRedisClient,
699
+ TRedisMode,
700
+ TRedisSentinelRole,
701
+ } from '@venizia/ignis-helpers';
702
+ ```
703
+
704
+ ## See Also
705
+
706
+ - [Helpers Index](../index) - All available helpers
707
+ - [Queue Helper](../queue/) - BullMQ uses the Redis helper as its connection backend
708
+ - [Socket.IO Component](/extensions/components/socket-io/) - Uses `duplicateClient` for pub/sub adapter
709
+ - [WebSocket Component](/extensions/components/websocket/) - Uses `duplicateClient` for pub/sub channels
710
+ - [Authorization Component](/extensions/components/authorization/) - Casbin enforcer uses the helper's `del`/`get`/`set` for cache operations
711
+ - [ioredis documentation](https://github.com/redis/ioredis) - Underlying Redis client
712
+ - [Redis Sentinel documentation](https://redis.io/docs/management/sentinel/) - Sentinel architecture reference
713
+ - [RedisJSON documentation](https://redis.io/docs/stack/json/) - Required for `j*` methods