From 36bcd257ba1dc622d8f04bddb89646d2349f1a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Olguncu?= <21091016+ogzhanolguncu@users.noreply.github.com> Date: Tue, 14 May 2024 11:29:13 +0300 Subject: [PATCH] feat: add lmpop command (#1061) * feat: add lmpop command * fix: add missing check --- pkg/auto-pipeline.test.ts | 39 +++++++---------- pkg/auto-pipeline.ts | 14 +++---- pkg/commands/lmpop.test.ts | 86 ++++++++++++++++++++++++++++++++++++++ pkg/commands/lmpop.ts | 18 ++++++++ pkg/commands/mod.ts | 1 + pkg/pipeline.ts | 7 ++++ pkg/redis.ts | 11 ++++- 7 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 pkg/commands/lmpop.test.ts create mode 100644 pkg/commands/lmpop.ts diff --git a/pkg/auto-pipeline.test.ts b/pkg/auto-pipeline.test.ts index 1b51cd3d..0fbf09fa 100644 --- a/pkg/auto-pipeline.test.ts +++ b/pkg/auto-pipeline.test.ts @@ -1,10 +1,9 @@ -import { Redis } from "../platforms/nodejs" +import { Redis } from "../platforms/nodejs"; import { keygen, newHttpClient } from "./test-utils"; import { afterEach, describe, expect, test } from "bun:test"; import { ScriptLoadCommand } from "./commands/script_load"; - const client = newHttpClient(); const { newKey, cleanup } = keygen(); @@ -17,10 +16,10 @@ describe("Auto pipeline", () => { const scriptHash = await new ScriptLoadCommand(["return 1"]).exec(client); const redis = Redis.autoPipeline({ - latencyLogging: false - }) + latencyLogging: false, + }); // @ts-expect-error pipelineCounter is not in type but accessible - expect(redis.pipelineCounter).toBe(0) + expect(redis.pipelineCounter).toBe(0); // all the following commands are in a single pipeline call const result = await Promise.all([ @@ -143,19 +142,18 @@ describe("Auto pipeline", () => { redis.zscore(newKey(), "member"), redis.zunionstore(newKey(), 1, [newKey()]), redis.zunion(1, [newKey()]), - redis.json.set(newKey(), "$", { hello: "world" }) - ]) + redis.json.set(newKey(), "$", { hello: "world" }), + ]); expect(result).toBeTruthy(); - expect(result.length).toBe(120); // returns + expect(result.length).toBe(120); // returns // @ts-expect-error pipelineCounter is not in type but accessible120 results expect(redis.pipelineCounter).toBe(1); }); test("should group async requests with sync requests", async () => { - const redis = Redis.autoPipeline({ - latencyLogging: false - }) + latencyLogging: false, + }); // @ts-expect-error pipelineCounter is not in type but accessible expect(redis.pipelineCounter).toBe(0); @@ -168,21 +166,17 @@ describe("Auto pipeline", () => { // two get calls are added to the pipeline and pipeline // is executed since we called await - const [fooValue, bazValue] = await Promise.all([ - redis.get("foo"), - redis.get("baz") - ]); + const [fooValue, bazValue] = await Promise.all([redis.get("foo"), redis.get("baz")]); expect(fooValue).toBe("bar"); expect(bazValue).toBe(3); // @ts-expect-error pipelineCounter is not in type but accessible expect(redis.pipelineCounter).toBe(1); - }) + }); test("should execute a pipeline for each consecutive awaited command", async () => { - const redis = Redis.autoPipeline({ - latencyLogging: false + latencyLogging: false, }); // @ts-expect-error pipelineCounter is not in type but accessible expect(redis.pipelineCounter).toBe(0); @@ -202,13 +196,11 @@ describe("Auto pipeline", () => { expect(redis.pipelineCounter).toBe(3); expect([res1, res2, res3]).toEqual([1, 2, "OK"]); - }); test("should execute a single pipeline for several commands inside Promise.all", async () => { - const redis = Redis.autoPipeline({ - latencyLogging: false + latencyLogging: false, }); // @ts-expect-error pipelineCounter is not in type but accessible expect(redis.pipelineCounter).toBe(0); @@ -218,11 +210,10 @@ describe("Auto pipeline", () => { redis.incr("baz"), redis.incr("baz"), redis.set("foo", "bar"), - redis.get("foo") + redis.get("foo"), ]); // @ts-expect-error pipelineCounter is not in type but accessible expect(redis.pipelineCounter).toBe(1); expect(resArray).toEqual(["OK", 1, 2, "OK", "bar"]); - - }) + }); }); diff --git a/pkg/auto-pipeline.ts b/pkg/auto-pipeline.ts index 3cfdfea6..6de3304f 100644 --- a/pkg/auto-pipeline.ts +++ b/pkg/auto-pipeline.ts @@ -1,26 +1,24 @@ import { Command } from "./commands/command"; -import { CommandArgs } from "./types"; import { Pipeline } from "./pipeline"; import { Redis } from "./redis"; +import { CommandArgs } from "./types"; // will omit redis only commands since we call Pipeline in the background in auto pipeline -type redisOnly = Exclude +type redisOnly = Exclude; export function createAutoPipelineProxy(_redis: Redis) { - const redis = _redis as Redis & { autoPipelineExecutor: AutoPipelineExecutor; - } + }; if (!redis.autoPipelineExecutor) { redis.autoPipelineExecutor = new AutoPipelineExecutor(redis); } return new Proxy(redis, { - get: (target, prop: "pipelineCounter" | keyof Pipeline ) => { - + get: (target, prop: "pipelineCounter" | keyof Pipeline) => { // return pipelineCounter of autoPipelineExecutor - if (prop == "pipelineCounter") { + if (prop === "pipelineCounter") { return target.autoPipelineExecutor.pipelineCounter; } @@ -43,7 +41,7 @@ export class AutoPipelineExecutor { private indexInCurrentPipeline = 0; private redis: Redis; pipeline: Pipeline; // only to make sure that proxy can work - pipelineCounter: number = 0; // to keep track of how many times a pipeline was executed + pipelineCounter = 0; // to keep track of how many times a pipeline was executed constructor(redis: Redis) { this.redis = redis; diff --git a/pkg/commands/lmpop.test.ts b/pkg/commands/lmpop.test.ts new file mode 100644 index 00000000..a04a7853 --- /dev/null +++ b/pkg/commands/lmpop.test.ts @@ -0,0 +1,86 @@ +import { keygen, newHttpClient, randomID } from "../test-utils"; + +import { afterAll, describe, expect, test } from "bun:test"; + +import { LmPopCommand } from "./lmpop"; +import { LPushCommand } from "./lpush"; +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); + +describe("LMPOP", () => { + test("should pop elements from the left-most end of the list", async () => { + const key = newKey(); + const lpushElement1 = { name: randomID(), surname: randomID() }; + const lpushElement2 = { name: randomID(), surname: randomID() }; + + await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client); + + const result = await new LmPopCommand<{ name: string; surname: string }>([ + 1, + [key], + "LEFT", + 2, + ]).exec(client); + + expect(result?.[1][0].name).toEqual(lpushElement2.name); + }); + + test("should pop elements from the right-most end of the list", async () => { + const key = newKey(); + const lpushElement1 = randomID(); + const lpushElement2 = randomID(); + + await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client); + + const result = await new LmPopCommand([1, [key], "RIGHT", 2]).exec(client); + + expect(result?.[1][0]).toEqual(lpushElement1); + }); + + test("should pop elements from the first list then second list", async () => { + const key = newKey(); + const lpushElement1 = randomID(); + const lpushElement2 = randomID(); + + const key2 = newKey(); + const lpushElement2_1 = randomID(); + const lpushElement2_2 = randomID(); + + await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client); + await new LPushCommand([key2, lpushElement2_1, lpushElement2_2]).exec(client); + + const result = await new LmPopCommand([2, [key, key2], "RIGHT", 4]).exec(client); + expect(result).toEqual([key, [lpushElement1, lpushElement2]]); + + const result1 = await new LmPopCommand([2, [key, key2], "RIGHT", 4]).exec(client); + expect(result1).toEqual([key2, [lpushElement2_1, lpushElement2_2]]); + }); + + test("should return null after first attempt", async () => { + const key = newKey(); + const lpushElement1 = randomID(); + const lpushElement2 = randomID(); + + await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client); + + await new LmPopCommand([1, [key], "LEFT", 2]).exec(client); + + const result1 = await new LmPopCommand([1, [key], "LEFT", 2]).exec(client); + + expect(result1).toBeNull(); + }); + + test("should return without count", async () => { + const key = newKey(); + const lpushElement1 = randomID(); + const lpushElement2 = randomID(); + + await new LPushCommand([key, lpushElement1, lpushElement2]).exec(client); + + const result1 = await new LmPopCommand([1, [key], "LEFT"]).exec(client); + + expect(result1).toEqual([key, [lpushElement2]]); + }); +}); diff --git a/pkg/commands/lmpop.ts b/pkg/commands/lmpop.ts new file mode 100644 index 00000000..aec9acb8 --- /dev/null +++ b/pkg/commands/lmpop.ts @@ -0,0 +1,18 @@ +import { Command, CommandOptions } from "./command"; + +/** + * @see https://redis.io/commands/lmpop + */ +export class LmPopCommand extends Command< + [string, TValues[]] | null, + [string, TValues[]] | null +> { + constructor( + cmd: [numkeys: number, keys: string[], "LEFT" | "RIGHT", count?: number], + opts?: CommandOptions<[string, TValues[]] | null, [string, TValues[]] | null>, + ) { + const [numkeys, keys, direction, count] = cmd; + + super(["LMPOP", numkeys, ...keys, direction, ...(count ? ["COUNT", count] : [])], opts); + } +} diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index 92e3218a..79abd9e2 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -73,6 +73,7 @@ export * from "./linsert"; export * from "./llen"; export * from "./lmove"; export * from "./lpop"; +export * from "./lmpop"; export * from "./lpos"; export * from "./lpush"; export * from "./lpushx"; diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index ae5d198b..18194eea 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -81,6 +81,7 @@ import { LRemCommand, LSetCommand, LTrimCommand, + LmPopCommand, MGetCommand, MSetCommand, MSetNXCommand, @@ -651,6 +652,12 @@ export class Pipeline[] = []> { lpop = (...args: CommandArgs) => this.chain(new LPopCommand(args, this.commandOptions)); + /** + * @see https://redis.io/commands/lmpop + */ + lmpop = (...args: CommandArgs) => + this.chain(new LmPopCommand(args, this.commandOptions)); + /** * @see https://redis.io/commands/lpos */ diff --git a/pkg/redis.ts b/pkg/redis.ts index 251a9c3a..723224b3 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -1,3 +1,4 @@ +import { createAutoPipelineProxy } from "../pkg/auto-pipeline"; import { AppendCommand, BitCountCommand, @@ -81,6 +82,7 @@ import { LRemCommand, LSetCommand, LTrimCommand, + LmPopCommand, MGetCommand, MSetCommand, MSetNXCommand, @@ -175,7 +177,6 @@ import { Requester, UpstashRequest, UpstashResponse } from "./http"; import { Pipeline } from "./pipeline"; import { Script } from "./script"; import type { CommandArgs, RedisOptions, Telemetry } from "./types"; -import { AutoPipelineExecutor, createAutoPipelineProxy } from "../pkg/auto-pipeline" // See https://github.com/upstash/upstash-redis/issues/342 // why we need this export @@ -380,7 +381,7 @@ export class Redis { }); autoPipeline = () => { - return createAutoPipelineProxy(this) + return createAutoPipelineProxy(this); }; /** @@ -743,6 +744,12 @@ export class Redis { lpop = (...args: CommandArgs) => new LPopCommand(args, this.opts).exec(this.client); + /** + * @see https://redis.io/commands/lmpop + */ + lmpop = (...args: CommandArgs) => + new LmPopCommand(args, this.opts).exec(this.client); + /** * @see https://redis.io/commands/lpos */