@venizia/ignis-docs 0.0.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +46 -22
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +40 -31
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +163 -92
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +49 -31
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -1,46 +1,65 @@
|
|
|
1
1
|
# Redis
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
Backed by **ioredis** under the hood.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|-------|---------|----------|
|
|
9
|
-
| **`DefaultRedisHelper`** | `BaseHelper` | Base class with unified API for all Redis operations |
|
|
10
|
-
| **`RedisHelper`** | `DefaultRedisHelper` | Single Redis instance with auto-connect and retry strategy |
|
|
11
|
-
| **`RedisClusterHelper`** | `DefaultRedisHelper` | Redis cluster with multi-node support |
|
|
7
|
+
## 1. Overview
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
### When to use which topology
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
RedisClusterHelper,
|
|
21
|
-
} from '@venizia/ignis-helpers';
|
|
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 |
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
|
|
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:
|
|
34
43
|
|
|
35
|
-
|
|
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?)` |
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
## 3. Construction - Single
|
|
38
57
|
|
|
39
58
|
```typescript
|
|
40
|
-
import {
|
|
59
|
+
import { RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
41
60
|
|
|
42
|
-
const redis = new
|
|
43
|
-
name: '
|
|
61
|
+
const redis = new RedisSingleHelper({
|
|
62
|
+
name: 'cache',
|
|
44
63
|
host: 'localhost',
|
|
45
64
|
port: 6379,
|
|
46
65
|
password: 'secret',
|
|
@@ -49,440 +68,646 @@ const redis = new RedisHelper({
|
|
|
49
68
|
maxRetry: 5,
|
|
50
69
|
|
|
51
70
|
onInitialized: ({ name, helper }) => {
|
|
52
|
-
console.log(`
|
|
71
|
+
console.log(`[${name}] initialized`);
|
|
53
72
|
},
|
|
54
73
|
onConnected: ({ name }) => {
|
|
55
|
-
console.log(`
|
|
74
|
+
console.log(`[${name}] connected`);
|
|
56
75
|
},
|
|
57
76
|
onReady: ({ name }) => {
|
|
58
|
-
console.log(`
|
|
77
|
+
console.log(`[${name}] ready`);
|
|
59
78
|
},
|
|
60
79
|
onError: ({ name, error }) => {
|
|
61
|
-
console.error(`
|
|
80
|
+
console.error(`[${name}] error`, error);
|
|
62
81
|
},
|
|
63
82
|
});
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
### IRedisSingleHelperOptions
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|--------|------|----------|---------|-------------|
|
|
70
|
-
| `name` | `string` | Yes | -- | Unique identifier for this client instance |
|
|
71
|
-
| `host` | `string` | Yes | -- | Redis server hostname |
|
|
72
|
-
| `port` | `string \| number` | Yes | -- | Redis server port |
|
|
73
|
-
| `password` | `string` | Yes | -- | Redis password |
|
|
74
|
-
| `user` | `string` | No | -- | Redis username (ACL authentication) |
|
|
75
|
-
| `database` | `number` | No | `0` | Redis database index |
|
|
76
|
-
| `autoConnect` | `boolean` | No | `true` | Connect immediately on creation. When `false`, uses ioredis `lazyConnect` mode |
|
|
77
|
-
| `maxRetry` | `number` | No | `0` | Maximum reconnection attempts. `0` = unlimited retries. Values below `0` disable retry entirely |
|
|
78
|
-
| `onInitialized` | `(opts: { name: string; helper: DefaultRedisHelper }) => void` | No | -- | Called synchronously immediately after client construction |
|
|
79
|
-
| `onConnected` | `(opts: { name: string; helper: DefaultRedisHelper }) => void` | No | -- | Called when the TCP connection is established |
|
|
80
|
-
| `onReady` | `(opts: { name: string; helper: DefaultRedisHelper }) => void` | No | -- | Called when the client is ready to accept commands |
|
|
81
|
-
| `onError` | `(opts: { name: string; helper: DefaultRedisHelper; error: any }) => void` | No | -- | Called on connection or command errors |
|
|
87
|
+
Combines `IRedisSingleHelperProps` and `IRedisHelperCallbacks`.
|
|
82
88
|
|
|
83
|
-
|
|
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 |
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
### Retry strategy
|
|
86
105
|
|
|
87
|
-
|
|
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.
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
## 4. Construction - Cluster
|
|
90
109
|
|
|
91
110
|
```typescript
|
|
92
111
|
import { RedisClusterHelper } from '@venizia/ignis-helpers';
|
|
93
112
|
|
|
94
113
|
const cluster = new RedisClusterHelper({
|
|
95
|
-
name: '
|
|
114
|
+
name: 'cache-cluster',
|
|
96
115
|
nodes: [
|
|
97
116
|
{ host: 'redis-node-1', port: 7000 },
|
|
98
|
-
{ host: 'redis-node-2', port: 7001
|
|
117
|
+
{ host: 'redis-node-2', port: 7001 },
|
|
99
118
|
{ host: 'redis-node-3', port: 7002 },
|
|
100
119
|
],
|
|
101
120
|
clusterOptions: {
|
|
102
121
|
redisOptions: { password: 'cluster-password' },
|
|
103
122
|
},
|
|
104
|
-
|
|
105
123
|
onReady: ({ name }) => {
|
|
106
|
-
console.log(`
|
|
124
|
+
console.log(`[${name}] cluster ready`);
|
|
107
125
|
},
|
|
108
126
|
});
|
|
109
127
|
```
|
|
110
128
|
|
|
111
|
-
|
|
129
|
+
### IRedisClusterHelperOptions
|
|
112
130
|
|
|
113
|
-
|
|
|
114
|
-
|
|
115
|
-
| `name` | `string` | Yes |
|
|
116
|
-
| `nodes` | `Array<{ host: string; port: string \| number; password?: string }>` | Yes |
|
|
117
|
-
| `clusterOptions` | `ClusterOptions` | No |
|
|
118
|
-
| `onInitialized`
|
|
119
|
-
| `onConnected` | `(opts: { name: string; helper: DefaultRedisHelper }) => void` | No | -- | Called when connected |
|
|
120
|
-
| `onReady` | `(opts: { name: string; helper: DefaultRedisHelper }) => void` | No | -- | Called when ready |
|
|
121
|
-
| `onError` | `(opts: { name: string; helper: DefaultRedisHelper; error: any }) => void` | No | -- | Called on errors |
|
|
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 |
|
|
122
137
|
|
|
123
|
-
|
|
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.
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
`duplicateClient()` on a cluster creates a **new** `Cluster` instance from the same startup nodes and options (because ioredis `Cluster` does not implement `.duplicate()`).
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
interface IRedisHelperCallbacks {
|
|
129
|
-
onInitialized?: (opts: { name: string; helper: DefaultRedisHelper }) => void;
|
|
130
|
-
onConnected?: (opts: { name: string; helper: DefaultRedisHelper }) => void;
|
|
131
|
-
onReady?: (opts: { name: string; helper: DefaultRedisHelper }) => void;
|
|
132
|
-
onError?: (opts: { name: string; helper: DefaultRedisHelper; error: any }) => void;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
| Callback | Redis Event | When |
|
|
137
|
-
|----------|-------------|------|
|
|
138
|
-
| `onInitialized` | -- | Immediately after client construction (synchronous) |
|
|
139
|
-
| `onConnected` | `connect` | TCP connection established |
|
|
140
|
-
| `onReady` | `ready` | Client ready to accept commands |
|
|
141
|
-
| `onError` | `error` | Connection or command error |
|
|
142
|
+
## 5. Construction - Sentinel
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
### What Redis Sentinel is
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
Redis Sentinel is a high-availability (HA) architecture made of three process types:
|
|
146
147
|
|
|
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
|
+
```
|
|
148
159
|
|
|
149
|
-
|
|
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.
|
|
150
163
|
|
|
151
|
-
|
|
152
|
-
// Manual connect (when autoConnect is false)
|
|
153
|
-
const connected = await redis.connect();
|
|
154
|
-
// => true if status becomes 'ready', false if already connected/connecting
|
|
164
|
+
### Why your app connects to sentinels, not the master
|
|
155
165
|
|
|
156
|
-
|
|
157
|
-
const disconnected = await redis.disconnect();
|
|
158
|
-
// => true if quit succeeded
|
|
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.
|
|
159
167
|
|
|
160
|
-
|
|
161
|
-
const pong = await redis.ping();
|
|
162
|
-
// => 'PONG'
|
|
168
|
+
### Constructor
|
|
163
169
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
});
|
|
167
188
|
```
|
|
168
189
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
```
|
|
171
286
|
|
|
172
|
-
|
|
287
|
+
Connect and verify failover:
|
|
173
288
|
|
|
174
289
|
```typescript
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
});
|
|
177
300
|
|
|
178
|
-
|
|
179
|
-
await redis.
|
|
301
|
+
await redis.set({ key: 'hello', value: 'world' });
|
|
302
|
+
const val = await redis.get({ key: 'hello' });
|
|
303
|
+
console.log(val); // "world"
|
|
180
304
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
305
|
+
// Stop the master container - wait ~5 s - the replica is promoted.
|
|
306
|
+
// The same redis instance will reconnect automatically and continue working.
|
|
307
|
+
```
|
|
184
308
|
|
|
185
|
-
|
|
186
|
-
const parsed = await redis.get({
|
|
187
|
-
key: 'user:1',
|
|
188
|
-
transform: (input) => JSON.parse(input),
|
|
189
|
-
});
|
|
309
|
+
## 6. Selecting a Topology - the Factory
|
|
190
310
|
|
|
191
|
-
|
|
192
|
-
const str = await redis.getString({ key: 'user:1' });
|
|
311
|
+
`createRedisHelper` is an overloaded factory that returns the concrete type when `mode` is a literal.
|
|
193
312
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
});
|
|
197
324
|
|
|
198
|
-
//
|
|
199
|
-
|
|
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
|
+
});
|
|
200
333
|
```
|
|
201
334
|
|
|
202
|
-
|
|
335
|
+
Switch by environment:
|
|
203
336
|
|
|
204
337
|
```typescript
|
|
205
|
-
|
|
206
|
-
await redis.mset({
|
|
207
|
-
payload: [
|
|
208
|
-
{ key: 'user:1', value: { name: 'Alice' } },
|
|
209
|
-
{ key: 'user:2', value: { name: 'Bob' } },
|
|
210
|
-
],
|
|
211
|
-
});
|
|
338
|
+
import { createRedisHelper, RedisModes, type IRedisHelper } from '@venizia/ignis-helpers';
|
|
212
339
|
|
|
213
|
-
|
|
214
|
-
const values = await redis.mget({ keys: ['user:1', 'user:2'] });
|
|
215
|
-
// => ['{"name":"Alice"}', '{"name":"Bob"}']
|
|
340
|
+
const mode = (process.env.REDIS_MODE ?? RedisModes.SINGLE) as typeof RedisModes.SINGLE;
|
|
216
341
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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!,
|
|
221
348
|
});
|
|
349
|
+
```
|
|
222
350
|
|
|
223
|
-
|
|
224
|
-
|
|
351
|
+
| `mode` value | Returns |
|
|
352
|
+
|---|---|
|
|
353
|
+
| `RedisModes.SINGLE` (`'single'`) | `RedisSingleHelper` |
|
|
354
|
+
| `RedisModes.CLUSTER` (`'cluster'`) | `RedisClusterHelper` |
|
|
355
|
+
| `RedisModes.SENTINEL` (`'sentinel'`) | `RedisSentinelHelper` |
|
|
225
356
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
357
|
+
Unknown mode throws an `ApplicationError`.
|
|
358
|
+
|
|
359
|
+
## 7. Lifecycle and Events
|
|
229
360
|
|
|
230
|
-
|
|
231
|
-
> `mSet()`, `mGet()`, `hSet()`, and `hGetAll()` are camelCase aliases for `mset()`, `mget()`, `hset()`, and `hgetall()` respectively. Both forms are valid.
|
|
361
|
+
### autoConnect and manual connect
|
|
232
362
|
|
|
233
|
-
|
|
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.
|
|
234
364
|
|
|
235
365
|
```typescript
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
366
|
+
// Lazy connect
|
|
367
|
+
const redis = new RedisSingleHelper({
|
|
368
|
+
name: 'cache',
|
|
369
|
+
host: 'localhost',
|
|
370
|
+
port: 6379,
|
|
371
|
+
password: 'secret',
|
|
372
|
+
autoConnect: false,
|
|
240
373
|
});
|
|
241
374
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
key: 'session:abc',
|
|
245
|
-
value: { userId: 'u1' },
|
|
246
|
-
options: { log: true },
|
|
247
|
-
});
|
|
375
|
+
await redis.connect(); // resolves true when status === 'ready'
|
|
376
|
+
```
|
|
248
377
|
|
|
249
|
-
|
|
250
|
-
const session = await redis.hgetall({ key: 'session:abc' });
|
|
251
|
-
// => { userId: 'u1', token: 'tok123', createdAt: '2025-01-01' }
|
|
378
|
+
### Connection methods
|
|
252
379
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
```
|
|
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'` |
|
|
259
385
|
|
|
260
|
-
###
|
|
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
|
|
261
400
|
|
|
262
401
|
```typescript
|
|
263
|
-
|
|
264
|
-
const matchingKeys = await redis.keys({ key: 'user:*' });
|
|
265
|
-
// => ['user:1', 'user:2', ...]
|
|
402
|
+
const dedicated = redis.duplicateClient();
|
|
266
403
|
```
|
|
267
404
|
|
|
268
|
-
|
|
269
|
-
> `keys()` uses the Redis `KEYS` command, which scans the entire keyspace. Avoid using it in production on large databases -- prefer `SCAN` via `execute()` instead.
|
|
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.
|
|
270
406
|
|
|
271
|
-
|
|
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.
|
|
272
416
|
|
|
273
|
-
|
|
417
|
+
---
|
|
274
418
|
|
|
275
|
-
|
|
276
|
-
// Set a JSON document
|
|
277
|
-
await redis.jSet({ key: 'doc:1', path: '$', value: { name: 'Alice', scores: [10, 20] } });
|
|
419
|
+
### IRedisConnection - connection lifecycle
|
|
278
420
|
|
|
279
|
-
|
|
280
|
-
|
|
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 |
|
|
281
428
|
|
|
282
|
-
|
|
283
|
-
const scores = await redis.jGet({ key: 'doc:1', path: '$.scores' });
|
|
429
|
+
---
|
|
284
430
|
|
|
285
|
-
|
|
286
|
-
await redis.jDelete({ key: 'doc:1', path: '$.scores' });
|
|
431
|
+
### IRedisKey - key lifecycle and counters
|
|
287
432
|
|
|
288
|
-
|
|
289
|
-
|
|
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 |
|
|
290
444
|
|
|
291
|
-
|
|
292
|
-
await redis.jNumberIncreaseBy({ key: 'doc:1', path: '$.counter', value: 5 });
|
|
445
|
+
---
|
|
293
446
|
|
|
294
|
-
|
|
295
|
-
await redis.jStringAppend({ key: 'doc:1', path: '$.name', value: ' Smith' });
|
|
447
|
+
### IRedisKeyValue - string key-value
|
|
296
448
|
|
|
297
|
-
|
|
298
|
-
|
|
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 `[]` |
|
|
299
461
|
|
|
300
|
-
|
|
301
|
-
const popped = await redis.jPop({ key: 'doc:1', path: '$.tags' });
|
|
302
|
-
```
|
|
462
|
+
---
|
|
303
463
|
|
|
304
|
-
|
|
464
|
+
### IRedisHash - hash maps
|
|
305
465
|
|
|
306
|
-
| Method |
|
|
307
|
-
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
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 |
|
|
315
477
|
|
|
316
|
-
|
|
478
|
+
---
|
|
317
479
|
|
|
318
|
-
###
|
|
480
|
+
### IRedisSet - sets
|
|
319
481
|
|
|
320
|
-
|
|
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 |
|
|
321
489
|
|
|
322
|
-
|
|
323
|
-
// Subscribe to a topic
|
|
324
|
-
redis.subscribe({ topic: 'events' });
|
|
490
|
+
---
|
|
325
491
|
|
|
326
|
-
|
|
327
|
-
redis.getClient().on('message', (channel, message) => {
|
|
328
|
-
if (channel === 'events') {
|
|
329
|
-
console.log('Received:', JSON.parse(message));
|
|
330
|
-
}
|
|
331
|
-
});
|
|
492
|
+
### IRedisList - lists
|
|
332
493
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
});
|
|
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 });
|
|
338
519
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
useCompress: true,
|
|
520
|
+
subClient.subscribe({ topic: 'events' });
|
|
521
|
+
subClient.getClient().on('message', (channel, message) => {
|
|
522
|
+
const parsed = JSON.parse(message.toString());
|
|
523
|
+
console.log(channel, parsed);
|
|
344
524
|
});
|
|
345
525
|
|
|
346
|
-
|
|
347
|
-
redis.unsubscribe({ topic: 'events' });
|
|
526
|
+
await dataClient.set({ key: 'foo', value: 'bar' });
|
|
348
527
|
```
|
|
349
528
|
|
|
350
|
-
|
|
351
|
-
> When using Pub/Sub, the subscribing client enters subscriber mode and can only execute `SUBSCRIBE`, `PSUBSCRIBE`, `UNSUBSCRIBE`, `PUNSUBSCRIBE`, `PING`, and `QUIT` commands. Use a separate `RedisHelper` instance for Pub/Sub if you also need to perform regular data operations.
|
|
529
|
+
---
|
|
352
530
|
|
|
353
|
-
###
|
|
531
|
+
### IRedisJson - RedisJSON module
|
|
354
532
|
|
|
355
|
-
|
|
533
|
+
Requires the [RedisJSON module](https://redis.io/docs/stack/json/) on the server.
|
|
356
534
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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`) |
|
|
360
544
|
|
|
361
|
-
|
|
362
|
-
|
|
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)` |
|
|
363
552
|
|
|
364
|
-
|
|
553
|
+
```typescript
|
|
554
|
+
// Use SCAN instead of KEYS on large keyspaces
|
|
365
555
|
const [cursor, keys] = await redis.execute<[string, string[]]>(
|
|
366
|
-
'SCAN',
|
|
556
|
+
'SCAN',
|
|
557
|
+
[0, 'MATCH', 'user:*', 'COUNT', 100],
|
|
367
558
|
);
|
|
559
|
+
|
|
560
|
+
// Fetch server info
|
|
561
|
+
const info = await redis.execute<string>('INFO');
|
|
368
562
|
```
|
|
369
563
|
|
|
370
|
-
##
|
|
564
|
+
## 9. Using the Helper Across IGNIS
|
|
371
565
|
|
|
372
|
-
|
|
373
|
-
|--------|---------|-------------|
|
|
374
|
-
| **Connection** | | |
|
|
375
|
-
| `connect()` | `Promise<boolean>` | Manual connect (no-op if already connected/connecting/ready) |
|
|
376
|
-
| `disconnect()` | `Promise<boolean>` | Graceful disconnect via `QUIT` (no-op if already ended/closed) |
|
|
377
|
-
| `ping()` | `Promise<string>` | Health check, returns `'PONG'` |
|
|
378
|
-
| `getClient()` | `Redis \| Cluster` | Access the underlying ioredis client |
|
|
379
|
-
| **Key-Value** | | |
|
|
380
|
-
| `set<T>(opts)` | `Promise<void>` | Set a key with JSON-serialized value. Options: `{ key, value, options?: { log } }` |
|
|
381
|
-
| `get<T>(opts)` | `Promise<T \| null>` | Get raw value with optional transform. Options: `{ key, transform? }` |
|
|
382
|
-
| `getString(opts)` | `Promise<string \| null>` | Get raw string value. Options: `{ key }` |
|
|
383
|
-
| `getObject(opts)` | `Promise<object \| null>` | Get value parsed as JSON. Options: `{ key }` |
|
|
384
|
-
| `del(opts)` | `Promise<number>` | Delete one or more keys. Options: `{ keys: string[] }` |
|
|
385
|
-
| **Multi-Key** | | |
|
|
386
|
-
| `mset<T>(opts)` / `mSet<T>(opts)` | `Promise<void>` | Set multiple key-value pairs. Options: `{ payload: Array<{ key, value }>, options?: { log } }` |
|
|
387
|
-
| `mget<T>(opts)` / `mGet<T>(opts)` | `Promise<(T \| null)[]>` | Get multiple values with optional transform. Options: `{ keys, transform? }` |
|
|
388
|
-
| `getStrings(opts)` | `Promise<(string \| null)[]>` | Get multiple raw string values. Options: `{ keys }` |
|
|
389
|
-
| `getObjects(opts)` | `Promise<(object \| null)[]>` | Get multiple values parsed as JSON. Options: `{ keys }` |
|
|
390
|
-
| **Hashes** | | |
|
|
391
|
-
| `hset<T>(opts)` / `hSet<T>(opts)` | `Promise<number>` | Set hash fields. Options: `{ key, value: Record<string, unknown>, options?: { log } }` |
|
|
392
|
-
| `hgetall(opts)` / `hGetAll(opts)` | `Promise<Record<string, string> \| null>` | Get all hash fields with optional transform. Options: `{ key, transform? }` |
|
|
393
|
-
| **Key Scanning** | | |
|
|
394
|
-
| `keys(opts)` | `Promise<string[]>` | Find keys matching a glob pattern. Options: `{ key }` |
|
|
395
|
-
| **RedisJSON** | | |
|
|
396
|
-
| `jSet<T>(opts)` | `Promise<string \| null>` | Set a JSON document at path (`JSON.SET`) |
|
|
397
|
-
| `jGet<T>(opts)` | `Promise<T \| null>` | Get a JSON document or path (`JSON.GET`) |
|
|
398
|
-
| `jDelete(opts)` | `Promise<number>` | Delete a JSON path (`JSON.DEL`) |
|
|
399
|
-
| `jNumberIncreaseBy(opts)` | `Promise<string \| null>` | Increment a number at path (`JSON.NUMINCRBY`) |
|
|
400
|
-
| `jStringAppend(opts)` | `Promise<number[] \| null>` | Append to a string at path (`JSON.STRAPPEND`) |
|
|
401
|
-
| `jPush<T>(opts)` | `Promise<number[] \| null>` | Push to an array at path (`JSON.ARRAPPEND`) |
|
|
402
|
-
| `jPop<T>(opts)` | `Promise<T \| null>` | Pop from an array at path (`JSON.ARRPOP`) |
|
|
403
|
-
| **Pub/Sub** | | |
|
|
404
|
-
| `subscribe(opts)` | `void` | Subscribe to a topic. Options: `{ topic }` |
|
|
405
|
-
| `publish<T>(opts)` | `Promise<void>` | Publish to one or more topics with optional compression. Options: `{ topics, payload, useCompress? }` |
|
|
406
|
-
| `unsubscribe(opts)` | `void` | Unsubscribe from a topic. Options: `{ topic }` |
|
|
407
|
-
| **Raw** | | |
|
|
408
|
-
| `execute<R>(command, parameters?)` | `Promise<R>` | Execute any Redis command directly |
|
|
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.
|
|
409
567
|
|
|
410
|
-
|
|
568
|
+
### BullMQ (BullMQHelper)
|
|
411
569
|
|
|
412
|
-
|
|
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.
|
|
413
573
|
|
|
414
|
-
|
|
574
|
+
`RedisSingleHelper` (and `RedisSentinelHelper`) are BullMQ-compatible out of the box because the framework sets `maxRetriesPerRequest: null` in `buildDefaultOpts`.
|
|
415
575
|
|
|
416
|
-
|
|
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 });
|
|
417
581
|
|
|
418
|
-
|
|
582
|
+
const queue = BullMQHelper.newInstance({
|
|
583
|
+
queueName: 'email',
|
|
584
|
+
role: 'queue',
|
|
585
|
+
redisConnection: redis,
|
|
586
|
+
});
|
|
587
|
+
```
|
|
419
588
|
|
|
420
|
-
|
|
589
|
+
### Socket.IO component (SocketIOServerHelper)
|
|
421
590
|
|
|
422
|
-
|
|
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:
|
|
423
592
|
|
|
424
|
-
|
|
593
|
+
```typescript
|
|
594
|
+
import { AbstractRedisHelper, RedisSentinelHelper } from '@venizia/ignis-helpers';
|
|
425
595
|
|
|
426
|
-
|
|
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
|
+
```
|
|
427
600
|
|
|
428
|
-
|
|
601
|
+
See the Socket.IO component documentation for the full component registration and adapter setup.
|
|
429
602
|
|
|
430
|
-
###
|
|
603
|
+
### WebSocket component (WebSocket server and emitter)
|
|
431
604
|
|
|
432
|
-
|
|
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:
|
|
433
606
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
- Check firewall rules and network access between your application and the Redis server
|
|
437
|
-
- If using `autoConnect: false`, ensure you call `await redis.connect()` before any operations
|
|
438
|
-
- Verify `password` is correct (Redis returns a generic error for auth failure)
|
|
439
|
-
- For clusters, ensure all node addresses are reachable and the cluster is healthy (`redis-cli cluster info`)
|
|
607
|
+
```typescript
|
|
608
|
+
import { AbstractRedisHelper, RedisClusterHelper } from '@venizia/ignis-helpers';
|
|
440
609
|
|
|
441
|
-
|
|
610
|
+
const redis = new RedisClusterHelper({ name: 'ws-redis', nodes });
|
|
611
|
+
this.bind<AbstractRedisHelper>({ key: '@app/websocket/redis-connection' }).to(redis);
|
|
612
|
+
```
|
|
442
613
|
|
|
443
|
-
|
|
614
|
+
See the WebSocket component documentation for the full component registration.
|
|
444
615
|
|
|
445
|
-
|
|
616
|
+
### Casbin authorization enforcer
|
|
446
617
|
|
|
447
|
-
|
|
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`:
|
|
448
619
|
|
|
449
620
|
```typescript
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
```
|
|
452
634
|
|
|
453
|
-
|
|
454
|
-
subClient.subscribe({ topic: 'events' });
|
|
455
|
-
subClient.getClient().on('message', (channel, msg) => { /* ... */ });
|
|
635
|
+
### Binding into an application
|
|
456
636
|
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
}
|
|
459
654
|
```
|
|
460
655
|
|
|
461
|
-
|
|
656
|
+
## 10. Production Notes
|
|
462
657
|
|
|
463
|
-
**
|
|
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.
|
|
464
659
|
|
|
465
|
-
|
|
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.
|
|
466
661
|
|
|
467
|
-
**
|
|
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.
|
|
468
663
|
|
|
469
|
-
|
|
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.
|
|
470
665
|
|
|
471
|
-
- **
|
|
472
|
-
- [Services](/guides/core-concepts/services) - Using Redis in services
|
|
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.
|
|
473
667
|
|
|
474
|
-
- **
|
|
475
|
-
- [Helpers Index](../index) - All available helpers
|
|
476
|
-
- [Queue Helper](../queue/) - BullMQ uses Redis as backend
|
|
668
|
+
- **Avoid `keys()` in production.** The Redis `KEYS` command scans the entire keyspace and blocks the server. Use `execute('SCAN', [...])` for production pattern matching.
|
|
477
669
|
|
|
478
|
-
|
|
479
|
-
- [DataSources](/references/base/datasources) - Database connections
|
|
670
|
+
## Import Reference
|
|
480
671
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
485
705
|
|
|
486
|
-
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|