Skip to content

Commit 36bcd25

Browse files
feat: add lmpop command (#1061)
* feat: add lmpop command * fix: add missing check
1 parent c9dc7e8 commit 36bcd25

File tree

7 files changed

+142
-34
lines changed

7 files changed

+142
-34
lines changed

pkg/auto-pipeline.test.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { Redis } from "../platforms/nodejs"
1+
import { Redis } from "../platforms/nodejs";
22
import { keygen, newHttpClient } from "./test-utils";
33

44
import { afterEach, describe, expect, test } from "bun:test";
55
import { ScriptLoadCommand } from "./commands/script_load";
66

7-
87
const client = newHttpClient();
98

109
const { newKey, cleanup } = keygen();
@@ -17,10 +16,10 @@ describe("Auto pipeline", () => {
1716
const scriptHash = await new ScriptLoadCommand(["return 1"]).exec(client);
1817

1918
const redis = Redis.autoPipeline({
20-
latencyLogging: false
21-
})
19+
latencyLogging: false,
20+
});
2221
// @ts-expect-error pipelineCounter is not in type but accessible
23-
expect(redis.pipelineCounter).toBe(0)
22+
expect(redis.pipelineCounter).toBe(0);
2423

2524
// all the following commands are in a single pipeline call
2625
const result = await Promise.all([
@@ -143,19 +142,18 @@ describe("Auto pipeline", () => {
143142
redis.zscore(newKey(), "member"),
144143
redis.zunionstore(newKey(), 1, [newKey()]),
145144
redis.zunion(1, [newKey()]),
146-
redis.json.set(newKey(), "$", { hello: "world" })
147-
])
145+
redis.json.set(newKey(), "$", { hello: "world" }),
146+
]);
148147
expect(result).toBeTruthy();
149-
expect(result.length).toBe(120); // returns
148+
expect(result.length).toBe(120); // returns
150149
// @ts-expect-error pipelineCounter is not in type but accessible120 results
151150
expect(redis.pipelineCounter).toBe(1);
152151
});
153152

154153
test("should group async requests with sync requests", async () => {
155-
156154
const redis = Redis.autoPipeline({
157-
latencyLogging: false
158-
})
155+
latencyLogging: false,
156+
});
159157
// @ts-expect-error pipelineCounter is not in type but accessible
160158
expect(redis.pipelineCounter).toBe(0);
161159

@@ -168,21 +166,17 @@ describe("Auto pipeline", () => {
168166

169167
// two get calls are added to the pipeline and pipeline
170168
// is executed since we called await
171-
const [fooValue, bazValue] = await Promise.all([
172-
redis.get("foo"),
173-
redis.get("baz")
174-
]);
169+
const [fooValue, bazValue] = await Promise.all([redis.get("foo"), redis.get("baz")]);
175170

176171
expect(fooValue).toBe("bar");
177172
expect(bazValue).toBe(3);
178173
// @ts-expect-error pipelineCounter is not in type but accessible
179174
expect(redis.pipelineCounter).toBe(1);
180-
})
175+
});
181176

182177
test("should execute a pipeline for each consecutive awaited command", async () => {
183-
184178
const redis = Redis.autoPipeline({
185-
latencyLogging: false
179+
latencyLogging: false,
186180
});
187181
// @ts-expect-error pipelineCounter is not in type but accessible
188182
expect(redis.pipelineCounter).toBe(0);
@@ -202,13 +196,11 @@ describe("Auto pipeline", () => {
202196
expect(redis.pipelineCounter).toBe(3);
203197

204198
expect([res1, res2, res3]).toEqual([1, 2, "OK"]);
205-
206199
});
207200

208201
test("should execute a single pipeline for several commands inside Promise.all", async () => {
209-
210202
const redis = Redis.autoPipeline({
211-
latencyLogging: false
203+
latencyLogging: false,
212204
});
213205
// @ts-expect-error pipelineCounter is not in type but accessible
214206
expect(redis.pipelineCounter).toBe(0);
@@ -218,11 +210,10 @@ describe("Auto pipeline", () => {
218210
redis.incr("baz"),
219211
redis.incr("baz"),
220212
redis.set("foo", "bar"),
221-
redis.get("foo")
213+
redis.get("foo"),
222214
]);
223215
// @ts-expect-error pipelineCounter is not in type but accessible
224216
expect(redis.pipelineCounter).toBe(1);
225217
expect(resArray).toEqual(["OK", 1, 2, "OK", "bar"]);
226-
227-
})
218+
});
228219
});

pkg/auto-pipeline.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
import { Command } from "./commands/command";
2-
import { CommandArgs } from "./types";
32
import { Pipeline } from "./pipeline";
43
import { Redis } from "./redis";
4+
import { CommandArgs } from "./types";
55

66
// will omit redis only commands since we call Pipeline in the background in auto pipeline
7-
type redisOnly = Exclude<keyof Redis, keyof Pipeline>
7+
type redisOnly = Exclude<keyof Redis, keyof Pipeline>;
88

99
export function createAutoPipelineProxy(_redis: Redis) {
10-
1110
const redis = _redis as Redis & {
1211
autoPipelineExecutor: AutoPipelineExecutor;
13-
}
12+
};
1413

1514
if (!redis.autoPipelineExecutor) {
1615
redis.autoPipelineExecutor = new AutoPipelineExecutor(redis);
1716
}
1817

1918
return new Proxy(redis, {
20-
get: (target, prop: "pipelineCounter" | keyof Pipeline ) => {
21-
19+
get: (target, prop: "pipelineCounter" | keyof Pipeline) => {
2220
// return pipelineCounter of autoPipelineExecutor
23-
if (prop == "pipelineCounter") {
21+
if (prop === "pipelineCounter") {
2422
return target.autoPipelineExecutor.pipelineCounter;
2523
}
2624

@@ -43,7 +41,7 @@ export class AutoPipelineExecutor {
4341
private indexInCurrentPipeline = 0;
4442
private redis: Redis;
4543
pipeline: Pipeline; // only to make sure that proxy can work
46-
pipelineCounter: number = 0; // to keep track of how many times a pipeline was executed
44+
pipelineCounter = 0; // to keep track of how many times a pipeline was executed
4745

4846
constructor(redis: Redis) {
4947
this.redis = redis;

pkg/commands/lmpop.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { keygen, newHttpClient, randomID } from "../test-utils";
2+
3+
import { afterAll, describe, expect, test } from "bun:test";
4+
5+
import { LmPopCommand } from "./lmpop";
6+
import { LPushCommand } from "./lpush";
7+
const client = newHttpClient();
8+
9+
const { newKey, cleanup } = keygen();
10+
afterAll(cleanup);
11+
12+
describe("LMPOP", () => {
13+
test("should pop elements from the left-most end of the list", async () => {
14+
const key = newKey();
15+
const lpushElement1 = { name: randomID(), surname: randomID() };
16+
const lpushElement2 = { name: randomID(), surname: randomID() };
17+
18+
await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client);
19+
20+
const result = await new LmPopCommand<{ name: string; surname: string }>([
21+
1,
22+
[key],
23+
"LEFT",
24+
2,
25+
]).exec(client);
26+
27+
expect(result?.[1][0].name).toEqual(lpushElement2.name);
28+
});
29+
30+
test("should pop elements from the right-most end of the list", async () => {
31+
const key = newKey();
32+
const lpushElement1 = randomID();
33+
const lpushElement2 = randomID();
34+
35+
await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client);
36+
37+
const result = await new LmPopCommand<string>([1, [key], "RIGHT", 2]).exec(client);
38+
39+
expect(result?.[1][0]).toEqual(lpushElement1);
40+
});
41+
42+
test("should pop elements from the first list then second list", async () => {
43+
const key = newKey();
44+
const lpushElement1 = randomID();
45+
const lpushElement2 = randomID();
46+
47+
const key2 = newKey();
48+
const lpushElement2_1 = randomID();
49+
const lpushElement2_2 = randomID();
50+
51+
await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client);
52+
await new LPushCommand([key2, lpushElement2_1, lpushElement2_2]).exec(client);
53+
54+
const result = await new LmPopCommand<string>([2, [key, key2], "RIGHT", 4]).exec(client);
55+
expect(result).toEqual([key, [lpushElement1, lpushElement2]]);
56+
57+
const result1 = await new LmPopCommand<string>([2, [key, key2], "RIGHT", 4]).exec(client);
58+
expect(result1).toEqual([key2, [lpushElement2_1, lpushElement2_2]]);
59+
});
60+
61+
test("should return null after first attempt", async () => {
62+
const key = newKey();
63+
const lpushElement1 = randomID();
64+
const lpushElement2 = randomID();
65+
66+
await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client);
67+
68+
await new LmPopCommand([1, [key], "LEFT", 2]).exec(client);
69+
70+
const result1 = await new LmPopCommand([1, [key], "LEFT", 2]).exec(client);
71+
72+
expect(result1).toBeNull();
73+
});
74+
75+
test("should return without count", async () => {
76+
const key = newKey();
77+
const lpushElement1 = randomID();
78+
const lpushElement2 = randomID();
79+
80+
await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client);
81+
82+
const result1 = await new LmPopCommand([1, [key], "LEFT"]).exec(client);
83+
84+
expect(result1).toEqual([key, [lpushElement2]]);
85+
});
86+
});

pkg/commands/lmpop.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Command, CommandOptions } from "./command";
2+
3+
/**
4+
* @see https://redis.io/commands/lmpop
5+
*/
6+
export class LmPopCommand<TValues> extends Command<
7+
[string, TValues[]] | null,
8+
[string, TValues[]] | null
9+
> {
10+
constructor(
11+
cmd: [numkeys: number, keys: string[], "LEFT" | "RIGHT", count?: number],
12+
opts?: CommandOptions<[string, TValues[]] | null, [string, TValues[]] | null>,
13+
) {
14+
const [numkeys, keys, direction, count] = cmd;
15+
16+
super(["LMPOP", numkeys, ...keys, direction, ...(count ? ["COUNT", count] : [])], opts);
17+
}
18+
}

pkg/commands/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export * from "./linsert";
7373
export * from "./llen";
7474
export * from "./lmove";
7575
export * from "./lpop";
76+
export * from "./lmpop";
7677
export * from "./lpos";
7778
export * from "./lpush";
7879
export * from "./lpushx";

pkg/pipeline.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
LRemCommand,
8282
LSetCommand,
8383
LTrimCommand,
84+
LmPopCommand,
8485
MGetCommand,
8586
MSetCommand,
8687
MSetNXCommand,
@@ -651,6 +652,12 @@ export class Pipeline<TCommands extends Command<any, any>[] = []> {
651652
lpop = <TData>(...args: CommandArgs<typeof LPopCommand>) =>
652653
this.chain(new LPopCommand<TData>(args, this.commandOptions));
653654

655+
/**
656+
* @see https://redis.io/commands/lmpop
657+
*/
658+
lmpop = <TData>(...args: CommandArgs<typeof LmPopCommand>) =>
659+
this.chain(new LmPopCommand<TData>(args, this.commandOptions));
660+
654661
/**
655662
* @see https://redis.io/commands/lpos
656663
*/

pkg/redis.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createAutoPipelineProxy } from "../pkg/auto-pipeline";
12
import {
23
AppendCommand,
34
BitCountCommand,
@@ -81,6 +82,7 @@ import {
8182
LRemCommand,
8283
LSetCommand,
8384
LTrimCommand,
85+
LmPopCommand,
8486
MGetCommand,
8587
MSetCommand,
8688
MSetNXCommand,
@@ -175,7 +177,6 @@ import { Requester, UpstashRequest, UpstashResponse } from "./http";
175177
import { Pipeline } from "./pipeline";
176178
import { Script } from "./script";
177179
import type { CommandArgs, RedisOptions, Telemetry } from "./types";
178-
import { AutoPipelineExecutor, createAutoPipelineProxy } from "../pkg/auto-pipeline"
179180

180181
// See https://github.com/upstash/upstash-redis/issues/342
181182
// why we need this export
@@ -380,7 +381,7 @@ export class Redis {
380381
});
381382

382383
autoPipeline = () => {
383-
return createAutoPipelineProxy(this)
384+
return createAutoPipelineProxy(this);
384385
};
385386

386387
/**
@@ -743,6 +744,12 @@ export class Redis {
743744
lpop = <TData>(...args: CommandArgs<typeof LPopCommand>) =>
744745
new LPopCommand<TData>(args, this.opts).exec(this.client);
745746

747+
/**
748+
* @see https://redis.io/commands/lmpop
749+
*/
750+
lmpop = <TData>(...args: CommandArgs<typeof LmPopCommand>) =>
751+
new LmPopCommand<TData>(args, this.opts).exec(this.client);
752+
746753
/**
747754
* @see https://redis.io/commands/lpos
748755
*/

0 commit comments

Comments
 (0)