Skip to content

Commit 985fec4

Browse files
authored
Feature: ioredis (#179)
* feature: add support for ioredis * clean up * chore: update ioredis adapter * chore: update example with ioredis * chore: update pnpm lock * fix lock file * fix ioredis adapter and update example to correctly initialize
1 parent be30f59 commit 985fec4

File tree

9 files changed

+296
-9
lines changed

9 files changed

+296
-9
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,26 @@ const redisCacheHandler = createRedisHandler({
181181

182182
**Note:** Redis Cluster support is currently experimental and may have limitations or unexpected bugs. Use it with caution.
183183

184+
### Using ioredis
185+
186+
If you prefer using `ioredis` instead of `@redis/client`, you can use the `ioredisAdapter` helper.
187+
188+
`npm i ioredis`
189+
190+
```js
191+
import Redis from "ioredis";
192+
import createRedisHandler from "@fortedigital/nextjs-cache-handler/redis-strings";
193+
import { ioredisAdapter } from "@fortedigital/nextjs-cache-handler/helpers/ioredisAdapter";
194+
195+
const client = new Redis(process.env.REDIS_URL);
196+
const redisClient = ioredisAdapter(client);
197+
198+
const redisHandler = createRedisHandler({
199+
client: redisClient,
200+
keyPrefix: "my-app:",
201+
});
202+
```
203+
184204
---
185205

186206
### `local-lru`

examples/redis-minimal/.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
NEXT_PRIVATE_DEBUG_CACHE="1"
22
REDIS_URL="redis://localhost:6379"
33
REDIS_SINGLE_CONNECTION=true
4-
#INITIAL_CACHE_SET_ONLY_IF_NOT_EXISTS=true
4+
#INITIAL_CACHE_SET_ONLY_IF_NOT_EXISTS=true
5+
# REDIS_TYPE can be "redis" (default, uses @redis/client) or "ioredis"
6+
REDIS_TYPE="redis"

examples/redis-minimal/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
2121

2222
Modify `.env` file if you need to.
2323

24+
### Redis Client Configuration
25+
26+
The example supports both Redis clients:
27+
28+
- **@redis/client** (default): Set `REDIS_TYPE="redis"` or leave unset
29+
- **ioredis**: Set `REDIS_TYPE="ioredis"`
30+
31+
This allows you to test the `ioredisAdapter` functionality.
32+
2433
## Examples
2534

2635
- http://localhost:3000

examples/redis-minimal/cache-handler.mjs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
import { createClient } from "redis";
2+
import Redis from "ioredis";
23
import { PHASE_PRODUCTION_BUILD } from "next/constants.js";
34
import { CacheHandler } from "@fortedigital/nextjs-cache-handler";
45
import createLruHandler from "@fortedigital/nextjs-cache-handler/local-lru";
56
import createRedisHandler from "@fortedigital/nextjs-cache-handler/redis-strings";
67
import createCompositeHandler from "@fortedigital/nextjs-cache-handler/composite";
8+
import { ioredisAdapter } from "@fortedigital/nextjs-cache-handler/helpers/ioredisAdapter";
79

810
const isSingleConnectionModeEnabled = !!process.env.REDIS_SINGLE_CONNECTION;
11+
const redisType = process.env.REDIS_TYPE || "redis"; // "redis" or "ioredis"
912

1013
async function setupRedisClient() {
1114
if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
15+
let redisClient;
16+
1217
try {
13-
const redisClient = createClient({
14-
url: process.env.REDIS_URL,
15-
pingInterval: 10000,
16-
});
18+
if (redisType === "ioredis") {
19+
console.info(`Using ioredis client...`);
20+
const ioredisClient = new Redis(process.env.REDIS_URL);
21+
22+
// Wait for connection to be ready
23+
console.info("Connecting ioredis client...");
24+
await new Promise((resolve, reject) => {
25+
ioredisClient.once("ready", () => {
26+
console.info("ioredis client connected.");
27+
resolve();
28+
});
29+
ioredisClient.once("error", reject);
30+
});
31+
32+
redisClient = ioredisAdapter(ioredisClient);
33+
} else {
34+
console.info(`Using @redis/client...`);
35+
redisClient = createClient({
36+
url: process.env.REDIS_URL,
37+
pingInterval: 10000,
38+
});
39+
40+
console.info("Connecting Redis client...");
41+
await redisClient.connect();
42+
console.info("Redis client connected.");
43+
}
1744

1845
redisClient.on("error", (e) => {
1946
if (process.env.NEXT_PRIVATE_DEBUG_CACHE !== undefined) {
@@ -25,10 +52,6 @@ async function setupRedisClient() {
2552
}
2653
});
2754

28-
console.info("Connecting Redis client...");
29-
await redisClient.connect();
30-
console.info("Redis client connected.");
31-
3255
if (!redisClient.isReady) {
3356
console.error("Failed to initialize caching layer.");
3457
}

examples/redis-minimal/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@fortedigital/nextjs-cache-handler": "workspace:*",
17+
"ioredis": "^5.8.2",
1718
"next": "^15.5.6",
1819
"react": "^19.2.3",
1920
"react-dom": "^19.2.3",

packages/nextjs-cache-handler/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"./helpers/withAbortSignalProxy": {
5151
"require": "./dist/helpers/withAbortSignalProxy.cjs",
5252
"import": "./dist/helpers/withAbortSignalProxy.js"
53+
},
54+
"./helpers/ioredisAdapter": {
55+
"require": "./dist/helpers/ioredisAdapter.cjs",
56+
"import": "./dist/helpers/ioredisAdapter.js"
5357
}
5458
},
5559
"typesVersions": {
@@ -80,6 +84,9 @@
8084
],
8185
"helpers/withAbortSignalProxy": [
8286
"dist/helpers/withAbortSignalProxy.d.ts"
87+
],
88+
"helpers/ioredisAdapter": [
89+
"dist/helpers/ioredisAdapter.d.ts"
8390
]
8491
}
8592
},
@@ -106,6 +113,7 @@
106113
"eslint": "^8.57.1",
107114
"eslint-config-prettier": "^9.1.2",
108115
"globals": "^16.5.0",
116+
"ioredis": "^5.8.2",
109117
"jest": "^30.2.0",
110118
"prettier": "^3.7.4",
111119
"prettier-plugin-packagejson": "2.5.21",
@@ -118,8 +126,14 @@
118126
},
119127
"peerDependencies": {
120128
"@redis/client": ">= 5.5.6",
129+
"ioredis": ">= 5.0.0",
121130
"next": ">=15.2.4"
122131
},
132+
"peerDependenciesMeta": {
133+
"ioredis": {
134+
"optional": true
135+
}
136+
},
123137
"distTags": [
124138
"next15"
125139
],
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import type { RedisClientType } from "@redis/client";
2+
import type { Redis } from "ioredis";
3+
4+
/**
5+
* Adapter to make an ioredis client compatible with the interface expected by createRedisHandler.
6+
*
7+
* @param client - The ioredis client instance.
8+
* @returns A proxy that implements the subset of RedisClientType used by this library.
9+
*/
10+
export function ioredisAdapter(client: Redis): RedisClientType {
11+
return new Proxy(client, {
12+
get(target, prop, receiver) {
13+
if (prop === "isReady") {
14+
return target.status === "ready";
15+
}
16+
17+
if (prop === "hScan") {
18+
return async (
19+
key: string,
20+
cursor: string,
21+
options?: { COUNT?: number },
22+
) => {
23+
let result: [string, string[]];
24+
25+
if (options?.COUNT) {
26+
result = await target.hscan(key, cursor, "COUNT", options.COUNT);
27+
} else {
28+
result = await target.hscan(key, cursor);
29+
}
30+
31+
const [newCursor, items] = result;
32+
33+
const entries = [];
34+
for (let i = 0; i < items.length; i += 2) {
35+
entries.push({ field: items[i], value: items[i + 1] });
36+
}
37+
38+
return {
39+
cursor: newCursor,
40+
entries,
41+
};
42+
};
43+
}
44+
45+
if (prop === "hDel") {
46+
return async (key: string, fields: string | string[]) => {
47+
const args = Array.isArray(fields) ? fields : [fields];
48+
if (args.length === 0) {
49+
return 0;
50+
}
51+
return target.hdel(key, ...args);
52+
};
53+
}
54+
55+
if (prop === "unlink") {
56+
return async (keys: string | string[]) => {
57+
const args = Array.isArray(keys) ? keys : [keys];
58+
if (args.length === 0) {
59+
return 0;
60+
}
61+
return target.unlink(...args);
62+
};
63+
}
64+
65+
if (prop === "set") {
66+
return async (key: string, value: string, options?: any) => {
67+
if (!options) {
68+
return target.set(key, value);
69+
}
70+
71+
const extraArgs: (string | number)[] = [];
72+
73+
// Expiration options (mutually exclusive)
74+
if (options.EXAT) {
75+
extraArgs.push("EXAT", options.EXAT);
76+
} else if (options.PXAT) {
77+
extraArgs.push("PXAT", options.PXAT);
78+
} else if (options.EX) {
79+
extraArgs.push("EX", options.EX);
80+
} else if (options.PX) {
81+
extraArgs.push("PX", options.PX);
82+
} else if (options.KEEPTTL) {
83+
extraArgs.push("KEEPTTL");
84+
}
85+
86+
// Condition options (mutually exclusive with each other, but can be combined with expiration)
87+
if (options.NX) {
88+
extraArgs.push("NX");
89+
} else if (options.XX) {
90+
extraArgs.push("XX");
91+
}
92+
93+
// GET option (can be combined with others)
94+
if (options.GET) {
95+
extraArgs.push("GET");
96+
}
97+
98+
// Cast to a generic signature to avoid overload mismatch issues with dynamic args
99+
const setFn = target.set as unknown as (
100+
key: string,
101+
value: string | number,
102+
...args: (string | number)[]
103+
) => Promise<string | null>;
104+
105+
return setFn(key, value, ...extraArgs);
106+
};
107+
}
108+
109+
if (prop === "hmGet") {
110+
return async (key: string, fields: string | string[]) => {
111+
const args = Array.isArray(fields) ? fields : [fields];
112+
if (args.length === 0) {
113+
return [];
114+
}
115+
return target.hmget(key, ...args);
116+
};
117+
}
118+
119+
if (prop === "get") {
120+
return target.get.bind(target);
121+
}
122+
123+
if (prop === "expireAt") {
124+
return target.expireat.bind(target);
125+
}
126+
127+
if (prop === "hSet") {
128+
return target.hset.bind(target);
129+
}
130+
131+
if (prop === "hExists") {
132+
return target.hexists.bind(target);
133+
}
134+
135+
if (prop === "on") {
136+
return target.on.bind(target);
137+
}
138+
139+
if (prop === "destroy") {
140+
return async () => {
141+
await target.quit();
142+
};
143+
}
144+
145+
return Reflect.get(target, prop, receiver);
146+
},
147+
}) as unknown as RedisClientType;
148+
}

packages/nextjs-cache-handler/tsup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const tsup = defineConfig({
99
"src/helpers/redisClusterAdapter.ts",
1010
"src/helpers/withAbortSignal.ts",
1111
"src/helpers/withAbortSignalProxy.ts",
12+
"src/helpers/ioredisAdapter.ts",
1213
],
1314
splitting: false,
1415
outDir: "dist",

0 commit comments

Comments
 (0)