Skip to content

Commit 0ca12db

Browse files
authored
DX-2439: add sintercard (#1413)
* feat: add sintercard * fix: fmt * fix: add sleep between script load and evalsha to fix race condition with replication
1 parent e9461d3 commit 0ca12db

8 files changed

Lines changed: 172 additions & 0 deletions

File tree

.claude/CLAUDE.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# redis-js SDK
2+
3+
## Adding a New Redis Command
4+
5+
You need to touch **6 files** (2 new, 4 existing):
6+
7+
### 1. Create the command file: `pkg/commands/<command_name>.ts`
8+
9+
```typescript
10+
import type { CommandOptions } from "./command";
11+
import { Command } from "./command";
12+
/**
13+
* @see https://redis.io/commands/<command-name>
14+
*/
15+
export class MyCommand extends Command<TResult, TData> {
16+
constructor(cmd: [...args], opts?: CommandOptions<TResult, TData>) {
17+
super(["mycommand", ...builtArgs], opts);
18+
}
19+
}
20+
```
21+
22+
Key patterns:
23+
24+
- `Command<number, number>` for commands returning a count (SCARD, SINTERCARD, SINTERSTORE)
25+
- `Command<unknown[], TData[]>` for commands returning arrays (SINTER, SMEMBERS)
26+
- Constructor first param is a tuple of the command's Redis arguments
27+
- Constructor second param is always `CommandOptions`
28+
- Call `super(["commandname", ...args], opts)` to build the Redis command array
29+
- For optional params (like LIMIT), conditionally push to the command array:
30+
```typescript
31+
if (opts?.limit !== undefined) {
32+
command.push("LIMIT", opts.limit);
33+
}
34+
```
35+
- For commands taking a variable number of keys with a numkeys param, accept `keys: string[]` and spread: `["cmd", keys.length, ...keys]`
36+
37+
### 2. Create the test file: `pkg/commands/<command_name>.test.ts`
38+
39+
```typescript
40+
import { keygen, newHttpClient, randomID } from "../test-utils";
41+
import { afterAll, expect, test } from "bun:test";
42+
import { SAddCommand } from "./sadd"; // or whatever setup commands needed
43+
import { MyCommand } from "./my_command";
44+
45+
const client = newHttpClient();
46+
const { newKey, cleanup } = keygen();
47+
afterAll(cleanup);
48+
49+
test("description", async () => {
50+
const key = newKey();
51+
// setup
52+
const res = await new MyCommand([...args]).exec(client);
53+
expect(res).toEqual(expected);
54+
});
55+
```
56+
57+
Run tests with: `npx bun test pkg/commands/<command_name>.test.ts`
58+
59+
### 3. Add export to `pkg/commands/mod.ts`
60+
61+
Insert `export * from "./<command_name>";` in alphabetical order among existing exports.
62+
63+
### 4. Add type export to `pkg/commands/types.ts`
64+
65+
Insert `export { type MyCommand } from "./<command_name>";` in alphabetical order.
66+
67+
### 5. Add method to `pkg/redis.ts`
68+
69+
- Add `MyCommand` to the import block from `"./commands/mod"` (alphabetical within the S/Z/etc group)
70+
- Add method to the class (alphabetical among similar commands):
71+
72+
```typescript
73+
/**
74+
* @see https://redis.io/commands/<command-name>
75+
*/
76+
mycommand = (...args: CommandArgs<typeof MyCommand>) =>
77+
new MyCommand(args, this.opts).exec(this.client);
78+
```
79+
80+
### 6. Add method to `pkg/pipeline.ts`
81+
82+
- Same import addition as redis.ts
83+
- Add method using `this.chain()` instead of `.exec()`:
84+
85+
```typescript
86+
/**
87+
* @see https://redis.io/commands/<command-name>
88+
*/
89+
mycommand = (...args: CommandArgs<typeof MyCommand>) =>
90+
this.chain(new MyCommand(args, this.commandOptions));
91+
```
92+
93+
### Checklist
94+
95+
- [ ] `pkg/commands/<name>.ts` - Command class
96+
- [ ] `pkg/commands/<name>.test.ts` - Tests
97+
- [ ] `pkg/commands/mod.ts` - Add `export *` line
98+
- [ ] `pkg/commands/types.ts` - Add `export { type }` line
99+
- [ ] `pkg/redis.ts` - Add import + method
100+
- [ ] `pkg/pipeline.ts` - Add import + method
101+
- [ ] Run `npx bun test pkg/commands/<name>.test.ts` to verify

pkg/commands/evalshaRo.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ describe("without keys", () => {
1414
test("returns something", async () => {
1515
const value = randomID();
1616
const sha1 = await new ScriptLoadCommand([`return {ARGV[1], "${value}"}`]).exec(client);
17+
18+
// sleep 150 ms
19+
await new Promise((resolve) => setTimeout(resolve, 150));
20+
1721
const res = await new EvalshaROCommand([sha1, [], [value]]).exec(client);
1822
expect(res).toEqual([value, value]);
1923
});

pkg/commands/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export * from "./setex";
141141
export * from "./setnx";
142142
export * from "./setrange";
143143
export * from "./sinter";
144+
export * from "./sintercard";
144145
export * from "./sinterstore";
145146
export * from "./sismember";
146147
export * from "./smembers";

pkg/commands/sintercard.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { keygen, newHttpClient, randomID } from "../test-utils";
2+
3+
import { afterAll, expect, test } from "bun:test";
4+
import { SAddCommand } from "./sadd";
5+
import { SInterCardCommand } from "./sintercard";
6+
7+
const client = newHttpClient();
8+
9+
const { newKey, cleanup } = keygen();
10+
afterAll(cleanup);
11+
12+
test("returns the cardinality of the intersection", async () => {
13+
const key1 = newKey();
14+
const key2 = newKey();
15+
const member1 = randomID();
16+
const member2 = randomID();
17+
await new SAddCommand([key1, member1, member2]).exec(client);
18+
await new SAddCommand([key2, member1]).exec(client);
19+
const res = await new SInterCardCommand([[key1, key2]]).exec(client);
20+
expect(res).toEqual(1);
21+
});
22+
23+
test("with limit", async () => {
24+
const key1 = newKey();
25+
const key2 = newKey();
26+
const member1 = randomID();
27+
const member2 = randomID();
28+
await new SAddCommand([key1, member1, member2]).exec(client);
29+
await new SAddCommand([key2, member1, member2]).exec(client);
30+
const res = await new SInterCardCommand([[key1, key2], { limit: 1 }]).exec(client);
31+
expect(res).toEqual(1);
32+
});

pkg/commands/sintercard.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { CommandOptions } from "./command";
2+
import { Command } from "./command";
3+
/**
4+
* @see https://redis.io/commands/sintercard
5+
*/
6+
export class SInterCardCommand extends Command<number, number> {
7+
constructor(
8+
cmd: [keys: string[], opts?: { limit?: number }],
9+
cmdOpts?: CommandOptions<number, number>
10+
) {
11+
const [keys, opts] = cmd;
12+
13+
const command: unknown[] = ["sintercard", keys.length, ...keys];
14+
if (opts?.limit !== undefined) {
15+
command.push("LIMIT", opts.limit);
16+
}
17+
super(command, cmdOpts);
18+
}
19+
}

pkg/commands/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export { type SetExCommand } from "./setex";
125125
export { type SetNxCommand } from "./setnx";
126126
export { type SetRangeCommand } from "./setrange";
127127
export { type SInterCommand } from "./sinter";
128+
export { type SInterCardCommand } from "./sintercard";
128129
export { type SInterStoreCommand } from "./sinterstore";
129130
export { type SIsMemberCommand } from "./sismember";
130131
export { type SMembersCommand } from "./smembers";

pkg/pipeline.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ import {
138138
SDiffCommand,
139139
SDiffStoreCommand,
140140
SInterCommand,
141+
SInterCardCommand,
141142
SInterStoreCommand,
142143
SIsMemberCommand,
143144
SMIsMemberCommand,
@@ -1088,6 +1089,12 @@ export class Pipeline<TCommands extends Command<any, any>[] = []> {
10881089
sinter = (...args: CommandArgs<typeof SInterCommand>) =>
10891090
this.chain(new SInterCommand(args, this.commandOptions));
10901091

1092+
/**
1093+
* @see https://redis.io/commands/sintercard
1094+
*/
1095+
sintercard = (...args: CommandArgs<typeof SInterCardCommand>) =>
1096+
this.chain(new SInterCardCommand(args, this.commandOptions));
1097+
10911098
/**
10921099
* @see https://redis.io/commands/sinterstore
10931100
*/

pkg/redis.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ import {
143143
SDiffCommand,
144144
SDiffStoreCommand,
145145
SInterCommand,
146+
SInterCardCommand,
146147
SInterStoreCommand,
147148
SIsMemberCommand,
148149
SMIsMemberCommand,
@@ -1276,6 +1277,12 @@ export class Redis {
12761277
sinter = (...args: CommandArgs<typeof SInterCommand>) =>
12771278
new SInterCommand(args, this.opts).exec(this.client);
12781279

1280+
/**
1281+
* @see https://redis.io/commands/sintercard
1282+
*/
1283+
sintercard = (...args: CommandArgs<typeof SInterCardCommand>) =>
1284+
new SInterCardCommand(args, this.opts).exec(this.client);
1285+
12791286
/**
12801287
* @see https://redis.io/commands/sinterstore
12811288
*/

0 commit comments

Comments
 (0)