Skip to content

Commit f293b97

Browse files
committedMar 14, 2022
feat: support defining custom commands via constructor options
1 parent c267e16 commit f293b97

File tree

11 files changed

+257
-18
lines changed

11 files changed

+257
-18
lines changed
 

‎README.md

+28-8
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ redis.zrange("sortedSet", 0, 2, "WITHSCORES").then((elements) => {
143143
redis.set("mykey", "hello", "EX", 10);
144144
```
145145

146-
147146
See the `examples/` folder for more examples.
148147

149148
## Connect to Redis
@@ -164,7 +163,7 @@ new Redis({
164163
password: "my-top-secret",
165164
db: 0, // Defaults to 0
166165
});
167-
````
166+
```
168167

169168
You can also specify connection options as a [`redis://` URL](http://www.iana.org/assignments/uri-schemes/prov/redis) or [`rediss://` URL](https://www.iana.org/assignments/uri-schemes/prov/rediss) when using [TLS encryption](#tls-options):
170169

@@ -496,6 +495,8 @@ redis.myechoBuffer("k1", "k2", "a1", "a2", (err, result) => {
496495
redis.pipeline().set("foo", "bar").myecho("k1", "k2", "a1", "a2").exec();
497496
```
498497

498+
### Dynamic Keys
499+
499500
If the number of keys can't be determined when defining a command, you can
500501
omit the `numberOfKeys` property and pass the number of keys as the first argument
501502
when you call the command:
@@ -512,6 +513,25 @@ redis.echoDynamicKeyNumber(2, "k1", "k2", "a1", "a2", (err, result) => {
512513
});
513514
```
514515

516+
### As Constructor Options
517+
518+
Besides `defineCommand()`, you can also define custom commands with the `scripts` constructor option:
519+
520+
```javascript
521+
const redis = new Redis({
522+
scripts: {
523+
myecho: {
524+
numberOfKeys: 2,
525+
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
526+
},
527+
},
528+
});
529+
```
530+
531+
### TypeScript Usages
532+
533+
You can refer to [the example](examples/typescript/scripts.ts) for how to declare your custom commands.
534+
515535
## Transparent Key Prefixing
516536

517537
This feature allows you to specify a string that will automatically be prepended
@@ -769,7 +789,7 @@ const redis = new Redis({
769789

770790
Set maxRetriesPerRequest to `null` to disable this behavior, and every command will wait forever until the connection is alive again (which is the default behavior before ioredis v4).
771791

772-
### Reconnect on error
792+
### Reconnect on Error
773793

774794
Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on certain Redis errors using the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error:
775795

@@ -1020,7 +1040,7 @@ cluster.get("foo", (err, res) => {
10201040
- `slotsRefreshTimeout`: Milliseconds before a timeout occurs while refreshing slots from the cluster (default `1000`).
10211041
- `slotsRefreshInterval`: Milliseconds between every automatic slots refresh (default `5000`).
10221042
1023-
### Read-write splitting
1043+
### Read-Write Splitting
10241044
10251045
A typical redis cluster contains three or more masters and several slaves for each master. It's possible to scale out redis cluster by sending read queries to slaves and write queries to masters by setting the `scaleReads` option.
10261046

@@ -1049,7 +1069,7 @@ cluster.get("foo", (err, res) => {
10491069

10501070
**NB** In the code snippet above, the `res` may not be equal to "bar" because of the lag of replication between the master and slaves.
10511071

1052-
### Running commands to multiple nodes
1072+
### Running Commands to Multiple Nodes
10531073

10541074
Every command will be sent to exactly one node. For commands containing keys, (e.g. `GET`, `SET` and `HGETALL`), ioredis sends them to the node that serving the keys, and for other commands not containing keys, (e.g. `INFO`, `KEYS` and `FLUSHDB`), ioredis sends them to a random node.
10551075

@@ -1099,7 +1119,7 @@ const cluster = new Redis.Cluster(
10991119

11001120
This option is also useful when the cluster is running inside a Docker container.
11011121

1102-
### Transaction and pipeline in Cluster mode
1122+
### Transaction and Pipeline in Cluster Mode
11031123

11041124
Almost all features that are supported by `Redis` are also supported by `Redis.Cluster`, e.g. custom commands, transaction and pipeline.
11051125
However there are some differences when using transaction and pipeline in Cluster mode:
@@ -1178,7 +1198,7 @@ const cluster = new Redis.Cluster(
11781198
);
11791199
```
11801200

1181-
### Special note: AWS ElastiCache Clusters with TLS
1201+
### Special Note: Aws Elasticache Clusters with TLS
11821202

11831203
AWS ElastiCache for Redis (Clustered Mode) supports TLS encryption. If you use
11841204
this, you may encounter errors with invalid certificates. To resolve this
@@ -1223,7 +1243,7 @@ A pipeline will thus contain commands using different slots but that ultimately
12231243

12241244
Note that the same slot limitation within a single command still holds, as it is a Redis limitation.
12251245

1226-
### Example of automatic pipeline enqueuing
1246+
### Example of Automatic Pipeline Enqueuing
12271247

12281248
This sample code uses ioredis with automatic pipeline enabled.
12291249

‎examples/typescript/package-lock.json

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎examples/typescript/package.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "typescript",
3+
"version": "0.0.0",
4+
"description": "",
5+
"private": true,
6+
"main": "index.js",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"devDependencies": {
11+
"typescript": "^4.6.2"
12+
}
13+
}

‎examples/typescript/scripts.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Redis, { RedisCommander, Result, Callback } from "ioredis";
2+
const redis = new Redis();
3+
4+
/**
5+
* Define our command
6+
*/
7+
redis.defineCommand("myecho", {
8+
numberOfKeys: 1,
9+
lua: "return KEYS[1] .. ARGV[1]",
10+
});
11+
12+
// Add declarations
13+
declare module "ioredis" {
14+
interface RedisCommander<Context> {
15+
myecho(
16+
key: string,
17+
argv: string,
18+
callback?: Callback<string>
19+
): Result<string, Context>;
20+
}
21+
}
22+
23+
// Works with callbacks
24+
redis.myecho("key", "argv", (err, result) => {
25+
console.log("callback", result);
26+
});
27+
28+
// Works with Promises
29+
(async () => {
30+
console.log("promise", await redis.myecho("key", "argv"));
31+
})();
32+
33+
// Works with pipelining
34+
redis
35+
.pipeline()
36+
.myecho("key", "argv")
37+
.exec((err, result) => {
38+
console.log("pipeline", result);
39+
});

‎lib/Redis.ts

+6
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ class Redis extends Commander {
134134
this.connector = new StandaloneConnector(this.options);
135135
}
136136

137+
if (this.options.scripts) {
138+
Object.entries(this.options.scripts).forEach(([name, definition]) => {
139+
this.defineCommand(name, definition);
140+
});
141+
}
142+
137143
// end(or wait) -> connecting -> connect -> ready -> end
138144
if (this.options.lazyConnect) {
139145
this.setStatus("wait");

‎lib/cluster/ClusterOptions.ts

+8
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ export interface ClusterOptions extends CommanderOptions {
195195
* @default 60000
196196
*/
197197
maxScriptsCachingTime?: number;
198+
199+
/**
200+
* Custom LUA commands
201+
*/
202+
scripts?: Record<
203+
string,
204+
{ lua: string; numberOfKeys?: number; readOnly?: boolean }
205+
>;
198206
}
199207

200208
export const DEFAULT_CLUSTER_OPTIONS: ClusterOptions = {

‎lib/cluster/index.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { EventEmitter } from "events";
21
import { exists, hasFlag } from "@ioredis/commands";
2+
import { EventEmitter } from "events";
33
import { AbortError, RedisError } from "redis-errors";
44
import asCallback from "standard-as-callback";
5-
import Pipeline from "../Pipeline";
65
import Command from "../Command";
76
import ClusterAllFailedError from "../errors/ClusterAllFailedError";
7+
import Pipeline from "../Pipeline";
88
import Redis from "../Redis";
99
import ScanStream from "../ScanStream";
10+
import { addTransactionSupport, Transaction } from "../transaction";
1011
import { Callback, ScanStreamOptions, WriteableStream } from "../types";
1112
import {
1213
CONNECTION_CLOSED_ERROR_MSG,
@@ -36,7 +37,6 @@ import {
3637
weightSrvRecords,
3738
} from "./util";
3839
import Deque = require("denque");
39-
import { addTransactionSupport, Transaction } from "../transaction";
4040

4141
const debug = Debug("cluster");
4242

@@ -145,6 +145,12 @@ class Cluster extends Commander {
145145

146146
this.subscriber = new ClusterSubscriber(this.connectionPool, this);
147147

148+
if (this.options.scripts) {
149+
Object.entries(this.options.scripts).forEach(([name, definition]) => {
150+
this.defineCommand(name, definition);
151+
});
152+
}
153+
148154
if (this.options.lazyConnect) {
149155
this.setStatus("wait");
150156
} else {

‎lib/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export { default as Cluster } from "./cluster";
88
*/
99
export { default as Command } from "./Command";
1010

11+
/**
12+
* @ignore
13+
*/
14+
export {
15+
default as RedisCommander,
16+
Result,
17+
ClientContext,
18+
} from "./utils/RedisCommander";
19+
1120
/**
1221
* @ignore
1322
*/
@@ -31,6 +40,11 @@ export {
3140
SentinelIterator,
3241
} from "./connectors/SentinelConnector";
3342

43+
/**
44+
* @ignore
45+
*/
46+
export { Callback } from "./types";
47+
3448
// Type Exports
3549
export { SentinelAddress } from "./connectors/SentinelConnector";
3650
export { RedisOptions } from "./redis/RedisOptions";

‎lib/redis/RedisOptions.ts

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ interface CommonRedisOptions extends CommanderOptions {
3232
enableOfflineQueue?: boolean;
3333
enableReadyCheck?: boolean;
3434
lazyConnect?: boolean;
35+
scripts?: Record<
36+
string,
37+
{ lua: string; numberOfKeys?: number; readOnly?: boolean }
38+
>;
3539
}
3640

3741
export type RedisOptions = CommonRedisOptions &

‎test/functional/cluster/scripting.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import MockServer from "../../helpers/mock_server";
2+
import { expect } from "chai";
3+
import { Cluster } from "../../../lib";
4+
5+
describe("cluster:scripting", () => {
6+
it("should throw when not all keys in a pipeline command belong to the same slot", async () => {
7+
const lua = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}";
8+
const handler = (argv) => {
9+
if (argv[0] === "cluster" && argv[1] === "SLOTS") {
10+
return [
11+
[0, 12181, ["127.0.0.1", 30001]],
12+
[12182, 16383, ["127.0.0.1", 30002]],
13+
];
14+
}
15+
console.log(argv);
16+
if (argv[0] === "eval" && argv[1] === lua && argv[2] === "2") {
17+
return argv.slice(3);
18+
}
19+
};
20+
new MockServer(30001, handler);
21+
new MockServer(30002, handler);
22+
23+
const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], {
24+
scripts: { test: { lua, numberOfKeys: 2 }, testDynamic: { lua } },
25+
});
26+
27+
// @ts-expect-error
28+
expect(await cluster.test("{foo}1", "{foo}2", "argv1", "argv2")).to.eql([
29+
"{foo}1",
30+
"{foo}2",
31+
"argv1",
32+
"argv2",
33+
]);
34+
35+
expect(
36+
// @ts-expect-error
37+
await cluster.testDynamic(2, "{foo}1", "{foo}2", "argv1", "argv2")
38+
).to.eql(["{foo}1", "{foo}2", "argv1", "argv2"]);
39+
40+
cluster.disconnect();
41+
});
42+
});

‎test/functional/scripting.ts

+58-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ import * as sinon from "sinon";
44
import { getCommandsFromMonitor } from "../helpers/util";
55

66
describe("scripting", () => {
7+
it("accepts constructor options", async () => {
8+
const redis = new Redis({
9+
scripts: {
10+
test: {
11+
numberOfKeys: 2,
12+
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
13+
},
14+
testDynamic: {
15+
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
16+
},
17+
},
18+
});
19+
20+
// @ts-expect-error
21+
expect(await redis.test("k1", "k2", "a1", "a2")).to.eql([
22+
"k1",
23+
"k2",
24+
"a1",
25+
"a2",
26+
]);
27+
// @ts-expect-error
28+
expect(await redis.testDynamic(2, "k1", "k2", "a1", "a2")).to.eql([
29+
"k1",
30+
"k2",
31+
"a1",
32+
"a2",
33+
]);
34+
redis.disconnect();
35+
});
36+
737
describe("#numberOfKeys", () => {
838
it("should recognize the numberOfKeys property", (done) => {
939
const redis = new Redis();
@@ -13,7 +43,8 @@ describe("scripting", () => {
1343
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
1444
});
1545

16-
redis.test("k1", "k2", "a1", "a2", function (err, result) {
46+
// @ts-expect-error
47+
redis.test("k1", "k2", "a1", "a2", (err, result) => {
1748
expect(result).to.eql(["k1", "k2", "a1", "a2"]);
1849
redis.disconnect();
1950
done();
@@ -27,7 +58,8 @@ describe("scripting", () => {
2758
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
2859
});
2960

30-
redis.test(2, "k1", "k2", "a1", "a2", function (err, result) {
61+
// @ts-expect-error
62+
redis.test(2, "k1", "k2", "a1", "a2", (err, result) => {
3163
expect(result).to.eql(["k1", "k2", "a1", "a2"]);
3264
redis.disconnect();
3365
done();
@@ -42,7 +74,8 @@ describe("scripting", () => {
4274
lua: "return {ARGV[1],ARGV[2]}",
4375
});
4476

45-
redis.test("2", "a2", function (err, result) {
77+
// @ts-expect-error
78+
redis.test("2", "a2", (err, result) => {
4679
expect(result).to.eql(["2", "a2"]);
4780
redis.disconnect();
4881
done();
@@ -56,7 +89,8 @@ describe("scripting", () => {
5689
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
5790
});
5891

59-
redis.test("k1", "k2", "a1", "a2", function (err, result) {
92+
// @ts-expect-error
93+
redis.test("k1", "k2", "a1", "a2", function (err) {
6094
expect(err).to.be.instanceof(Error);
6195
expect(err.toString()).to.match(/value is not an integer/);
6296
redis.disconnect();
@@ -73,7 +107,8 @@ describe("scripting", () => {
73107
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
74108
});
75109

76-
redis.testBuffer("k1", "k2", "a1", "a2", function (err, result) {
110+
// @ts-expect-error
111+
redis.testBuffer("k1", "k2", "a1", "a2", (err, result) => {
77112
expect(result).to.eql([
78113
Buffer.from("k1"),
79114
Buffer.from("k2"),
@@ -96,8 +131,9 @@ describe("scripting", () => {
96131
redis
97132
.pipeline()
98133
.set("test", "pipeline")
134+
// @ts-expect-error
99135
.test("test")
100-
.exec(function (err, results) {
136+
.exec((err, results) => {
101137
expect(results).to.eql([
102138
[null, "OK"],
103139
[null, "pipeline"],
@@ -117,6 +153,7 @@ describe("scripting", () => {
117153
redis
118154
.pipeline()
119155
.set("test", "pipeline")
156+
// @ts-expect-error
120157
.test("test")
121158
.exec(function (err, results) {
122159
expect(err).to.eql(null);
@@ -131,9 +168,11 @@ describe("scripting", () => {
131168
const redis = new Redis();
132169

133170
redis.defineCommand("test", { lua: "return 1" });
171+
// @ts-expect-error
134172
await redis.test(0);
135173

136174
const commands = await getCommandsFromMonitor(redis, 1, () => {
175+
// @ts-expect-error
137176
return redis.test(0);
138177
});
139178

@@ -152,11 +191,15 @@ describe("scripting", () => {
152191
lua: 'return redis.call("get", KEYS[1])',
153192
});
154193

194+
// @ts-expect-error
155195
await redis.test("preload");
196+
// @ts-expect-error
156197
await redis.script("flush");
157198

158199
const commands = await getCommandsFromMonitor(redis, 5, async () => {
200+
// @ts-expect-error
159201
await redis.test("foo");
202+
// @ts-expect-error
160203
await redis.test("bar");
161204
});
162205

@@ -172,6 +215,7 @@ describe("scripting", () => {
172215
lua: 'return redis.call("get", KEYS[1])',
173216
});
174217

218+
// @ts-expect-error
175219
await redis.testGet("init");
176220

177221
redis.defineCommand("testSet", {
@@ -180,6 +224,7 @@ describe("scripting", () => {
180224
});
181225

182226
const commands = await getCommandsFromMonitor(redis, 5, () => {
227+
// @ts-expect-error
183228
return redis.pipeline().testGet("foo").testSet("foo").get("foo").exec();
184229
});
185230

@@ -196,10 +241,13 @@ describe("scripting", () => {
196241
lua: 'return redis.call("get", KEYS[1])',
197242
});
198243

244+
// @ts-expect-error
199245
await redis.test("preload");
246+
// @ts-expect-error
200247
await redis.script("flush");
201248
const spy = sinon.spy(redis, "sendCommand");
202249
const commands = await getCommandsFromMonitor(redis, 4, async () => {
250+
// @ts-expect-error
203251
const [a, b] = await redis.multi().test("foo").test("bar").exec();
204252

205253
expect(a[0].message).to.equal(
@@ -223,7 +271,9 @@ describe("scripting", () => {
223271
lua: 'return redis.call("get", KEYS[1])',
224272
});
225273

274+
// @ts-expect-error
226275
await redis.test("preload");
276+
// @ts-expect-error
227277
await redis.script("flush");
228278
const spy = sinon.spy(redis, "sendCommand");
229279
const commands = await getCommandsFromMonitor(redis, 4, async () => {
@@ -245,7 +295,8 @@ describe("scripting", () => {
245295
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
246296
});
247297

248-
redis.echo("k1", "k2", "a1", "a2", function (err, result) {
298+
// @ts-expect-error
299+
redis.echo("k1", "k2", "a1", "a2", (err, result) => {
249300
expect(result).to.eql(["foo:k1", "foo:k2", "a1", "a2"]);
250301
redis.disconnect();
251302
done();

0 commit comments

Comments
 (0)
Please sign in to comment.