@heady/redis-client 1.0.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 +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/redisClient.d.ts +39 -0
- package/dist/redisClient.js +76 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Redis Client
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Redis Client** is a lightweight, type-safe wrapper for [ioredis](https://github.com/luin/ioredis). It solves the problem of scaling Redis infrastructure by automatically routing **Write** operations to a Primary node and **Read** operations to Replica nodes.
|
|
8
|
+
|
|
9
|
+
## ๐ Table of Contents
|
|
10
|
+
- [Features](#-features)
|
|
11
|
+
- [Prerequisites](#-prerequisites)
|
|
12
|
+
- [Installation](#-installation)
|
|
13
|
+
- [Usage](#-usage)
|
|
14
|
+
- [Configuration](#-configuration)
|
|
15
|
+
- [Architecture](#-architecture)
|
|
16
|
+
- [Running Tests](#-running-tests)
|
|
17
|
+
- [Contributing](#-contributing)
|
|
18
|
+
- [License](#-license)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## โจ Features
|
|
23
|
+
|
|
24
|
+
* **Automatic Traffic Splitting:** Writes (`set`, `del`, `expire`) are sent to the Primary; Reads (`get`, `scan`, `hget`) are sent to Replicas.
|
|
25
|
+
* **Type Safety:** Built with TypeScript, providing full IntelliSense and type checking out of the box.
|
|
26
|
+
* **Performance:** Offloads expensive read operations (like `SCAN`) from your Master node to prevent blocking critical write traffic.
|
|
27
|
+
* **Flexible Access:** Exposes the underlying `ioredis` instances if you need to run custom or unsupported commands.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ๐ง Prerequisites
|
|
32
|
+
|
|
33
|
+
Before you begin, ensure you have met the following requirements:
|
|
34
|
+
* **Node.js** (v14 or higher)
|
|
35
|
+
* **Redis:** A Redis setup with at least one Master and one Replica (e.g., AWS ElastiCache Cluster Mode Disabled).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ๐ Installation
|
|
40
|
+
|
|
41
|
+
1. **Install the package and the peer dependency:**
|
|
42
|
+
```bash
|
|
43
|
+
npm install redis-client
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ๐ก Usage
|
|
48
|
+
|
|
49
|
+
``` typescript
|
|
50
|
+
import { Redis } from 'redis-client';
|
|
51
|
+
|
|
52
|
+
// 1. Initialize with separate endpoints
|
|
53
|
+
const redis = new Redis({
|
|
54
|
+
primary: {
|
|
55
|
+
host: 'primary-node.redis.aws.internal',
|
|
56
|
+
port: 6379,
|
|
57
|
+
},
|
|
58
|
+
replica: {
|
|
59
|
+
host: 'replica-node.redis.aws.internal',
|
|
60
|
+
port: 6379,
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
// โ๏ธ WRITE: Automatically routed to Primary
|
|
66
|
+
await redis.set('user:101', 'John Doe', 3600);
|
|
67
|
+
console.log('User saved to Primary node');
|
|
68
|
+
|
|
69
|
+
// ๐ READ: Automatically routed to Replica
|
|
70
|
+
const user = await redis.get('user:101');
|
|
71
|
+
console.log('User read from Replica node:', user);
|
|
72
|
+
|
|
73
|
+
// ๐งน CLEANUP
|
|
74
|
+
await redis.disconnect();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## โ๏ธ Configuration
|
|
83
|
+
|
|
84
|
+
The RedisClient constructor accepts a configuration object with two keys: primary and replica. Both accept standard ioredis options.
|
|
85
|
+
|
|
86
|
+
| Property | Type | Description |
|
|
87
|
+
|:-----|:--------:|------:|
|
|
88
|
+
| primary | RedisOptions | Configuration for the Master (Write) node. |
|
|
89
|
+
| replica | RedisOptions | Configuration for the Read Replica
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
Example Config Object:
|
|
93
|
+
```TypeScript
|
|
94
|
+
{
|
|
95
|
+
primary: { host: '127.0.0.1', port: 6379, password: 'auth' },
|
|
96
|
+
replica: { host: '127.0.0.1', port: 6380, password: 'auth' }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## ๐ Architecture
|
|
102
|
+
|
|
103
|
+
This library implements the Read/Write Splitting pattern:
|
|
104
|
+
|
|
105
|
+
1. Command Interception: The client checks the method being called (e.g., set vs get).
|
|
106
|
+
2. Routing: * Mutating commands โ Primary Connection
|
|
107
|
+
- Read-only commands โ Replica Connection
|
|
108
|
+
3. Result: This ensures high availability and prevents heavy read queries from slowing down data ingestion.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## ๐งช Running Tests
|
|
113
|
+
|
|
114
|
+
This project uses Jest with ts-jest for unit testing. The tests mock ioredis to ensure traffic is routed to the correct connection without needing a real Redis instance.
|
|
115
|
+
|
|
116
|
+
To run the test suite:
|
|
117
|
+
``` Bash
|
|
118
|
+
npm test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Expected Output:
|
|
122
|
+
```Plaintext
|
|
123
|
+
|
|
124
|
+
PASS test/client.test.ts
|
|
125
|
+
RedisClient
|
|
126
|
+
โ should create two separate Redis connections
|
|
127
|
+
โ set() should call primary node
|
|
128
|
+
โ get() should call replica node
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## ๐ค Contributing
|
|
132
|
+
|
|
133
|
+
Contributions are always welcome! Please follow these steps:
|
|
134
|
+
|
|
135
|
+
Fork the project.
|
|
136
|
+
|
|
137
|
+
Create your feature branch (git checkout -b feature/NewFeature).
|
|
138
|
+
|
|
139
|
+
Commit your changes (git commit -m 'Add some NewFeature').
|
|
140
|
+
|
|
141
|
+
Run tests to ensure no regressions (npm test).
|
|
142
|
+
|
|
143
|
+
Push to the branch.
|
|
144
|
+
|
|
145
|
+
Open a Pull Request.
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## ๐ License
|
|
149
|
+
|
|
150
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./redisClient"), exports);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Redis as RedisType } from "ioredis";
|
|
2
|
+
import { RedisConfig } from "./types";
|
|
3
|
+
export declare class RedisClient {
|
|
4
|
+
private primary;
|
|
5
|
+
private replica;
|
|
6
|
+
constructor(config: RedisConfig);
|
|
7
|
+
/**
|
|
8
|
+
* -------------------------
|
|
9
|
+
* WRITE OPERATIONS (Primary)
|
|
10
|
+
* -------------------------
|
|
11
|
+
*/
|
|
12
|
+
set(key: string, value: string | number, ttlSeconds?: number): Promise<string | null>;
|
|
13
|
+
hset(key: string, object: Record<string, string | number>): Promise<number>;
|
|
14
|
+
del(key: string): Promise<number>;
|
|
15
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
16
|
+
/**
|
|
17
|
+
* -------------------------
|
|
18
|
+
* READ OPERATIONS (Replica)
|
|
19
|
+
* -------------------------
|
|
20
|
+
*/
|
|
21
|
+
get(key: string): Promise<string | null>;
|
|
22
|
+
hget(key: string, field: string): Promise<string | null>;
|
|
23
|
+
hgetall(key: string): Promise<Record<string, string>>;
|
|
24
|
+
scan(cursor: string, matchPattern: string, count?: number): Promise<[string, string[]]>;
|
|
25
|
+
/**
|
|
26
|
+
* -------------------------
|
|
27
|
+
* UTILITIES
|
|
28
|
+
* -------------------------
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Execute a custom command on the Primary node manually
|
|
32
|
+
*/
|
|
33
|
+
getWriteClient(): RedisType;
|
|
34
|
+
/**
|
|
35
|
+
* Execute a custom command on the Replica node manually
|
|
36
|
+
*/
|
|
37
|
+
getReadClient(): RedisType;
|
|
38
|
+
disconnect(): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RedisClient = void 0;
|
|
7
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
|
+
class RedisClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
// Initialize Write Connection
|
|
11
|
+
this.primary = new ioredis_1.default(config.primary);
|
|
12
|
+
// Initialize Read Connection
|
|
13
|
+
this.replica = new ioredis_1.default(config.replica);
|
|
14
|
+
// Error handling to prevent crashing the app
|
|
15
|
+
this.primary.on("error", (err) => console.error("Redis Primary Error:", err));
|
|
16
|
+
this.replica.on("error", (err) => console.error("Redis Replica Error:", err));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* -------------------------
|
|
20
|
+
* WRITE OPERATIONS (Primary)
|
|
21
|
+
* -------------------------
|
|
22
|
+
*/
|
|
23
|
+
async set(key, value, ttlSeconds) {
|
|
24
|
+
if (ttlSeconds) {
|
|
25
|
+
return this.primary.set(key, value, "EX", ttlSeconds);
|
|
26
|
+
}
|
|
27
|
+
return this.primary.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
async hset(key, object) {
|
|
30
|
+
return this.primary.hset(key, object);
|
|
31
|
+
}
|
|
32
|
+
async del(key) {
|
|
33
|
+
return this.primary.del(key);
|
|
34
|
+
}
|
|
35
|
+
async expire(key, seconds) {
|
|
36
|
+
return this.primary.expire(key, seconds);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* -------------------------
|
|
40
|
+
* READ OPERATIONS (Replica)
|
|
41
|
+
* -------------------------
|
|
42
|
+
*/
|
|
43
|
+
async get(key) {
|
|
44
|
+
return this.replica.get(key);
|
|
45
|
+
}
|
|
46
|
+
async hget(key, field) {
|
|
47
|
+
return this.replica.hget(key, field);
|
|
48
|
+
}
|
|
49
|
+
async hgetall(key) {
|
|
50
|
+
return this.replica.hgetall(key);
|
|
51
|
+
}
|
|
52
|
+
async scan(cursor, matchPattern, count = 100) {
|
|
53
|
+
return this.replica.scan(cursor, "MATCH", matchPattern, "COUNT", count);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* -------------------------
|
|
57
|
+
* UTILITIES
|
|
58
|
+
* -------------------------
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Execute a custom command on the Primary node manually
|
|
62
|
+
*/
|
|
63
|
+
getWriteClient() {
|
|
64
|
+
return this.primary;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Execute a custom command on the Replica node manually
|
|
68
|
+
*/
|
|
69
|
+
getReadClient() {
|
|
70
|
+
return this.replica;
|
|
71
|
+
}
|
|
72
|
+
async disconnect() {
|
|
73
|
+
await Promise.all([this.primary.quit(), this.replica.quit()]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.RedisClient = RedisClient;
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heady/redis-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"description": "",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@types/node": "^25.0.3",
|
|
19
|
+
"ioredis": "^5.9.1",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/jest": "^30.0.0",
|
|
24
|
+
"jest": "^30.2.0",
|
|
25
|
+
"ts-jest": "^29.4.6"
|
|
26
|
+
}
|
|
27
|
+
}
|