Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkg/auto-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe("Auto pipeline", () => {
redis.getset(newKey(), "hello"),
redis.hdel(newKey(), "field"),
redis.hexists(newKey(), "field"),
redis.hexpire(newKey(), "field", 1),
redis.hget(newKey(), "field"),
redis.hgetall(newKey()),
redis.hincrby(newKey(), "field", 1),
Expand Down Expand Up @@ -152,7 +153,7 @@ describe("Auto pipeline", () => {
redis.json.merge(persistentKey3, "$.log", '"three"'),
]);
expect(result).toBeTruthy();
expect(result.length).toBe(125); // returns
expect(result.length).toBe(126); // returns
// @ts-expect-error pipelineCounter is not in type but accessible120 results
expect(redis.pipelineCounter).toBe(1);
});
Expand Down
174 changes: 174 additions & 0 deletions pkg/commands/hexpire.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { keygen, newHttpClient, randomID } from "../test-utils";

import { afterAll, describe, expect, test } from "bun:test";
import { HSetCommand } from "./hset";
import { HExpireCommand } from "./hexpire";
import { HGetCommand } from "./hget";
const client = newHttpClient();

const { newKey, cleanup } = keygen();
afterAll(cleanup);

test("expires a hash key correctly", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
const res = await new HExpireCommand([key, hashKey, 1]).exec(client);
expect(res).toEqual([1]);
await new Promise((res) => setTimeout(res, 2000));
const res2 = await new HGetCommand([key, hashKey]).exec(client);

expect(res2).toEqual(null);
});

describe("NX", () => {
test("should set expiry only when the field has no expiry", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
const res = await new HExpireCommand([key, [hashKey], 1, "NX"]).exec(client);
expect(res).toEqual([1]);
await new Promise((res) => setTimeout(res, 2000));
const res2 = await new HGetCommand([key, hashKey]).exec(client);

expect(res2).toEqual(null);
});

test("should not set expiry when the field has expiry", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 1000]).exec(client);
const res = await new HExpireCommand([key, hashKey, 1, "NX"]).exec(client);
expect(res).toEqual([0]);
});
});

describe("XX", () => {
test(
"should set expiry only when the field has an existing expiry",
async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 1]).exec(client);
const res = await new HExpireCommand([key, hashKey, 5, "XX"]).exec(client);
expect(res).toEqual([1]);
await new Promise((res) => setTimeout(res, 6000));
const res2 = await new HGetCommand([key, hashKey]).exec(client);
expect(res2).toEqual(null);
},
{
timeout: 7000,
}
);

test("should not set expiry when the field does not have an existing expiry", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
const res = await new HExpireCommand([key, hashKey, 5, "XX"]).exec(client);
expect(res).toEqual([0]);
});
});

describe("GT", () => {
test(
"should set expiry only when the new expiry is greater than current one",
async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 1]).exec(client);
const res = await new HExpireCommand([key, hashKey, 5, "GT"]).exec(client);
expect(res).toEqual([1]);
await new Promise((res) => setTimeout(res, 6000));
const res2 = await new HGetCommand([key, hashKey]).exec(client);
expect(res2).toEqual(null);
},
{
timeout: 7000,
}
);

test("should not set expiry when the new expiry is not greater than current one", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 10]).exec(client);
const res = await new HExpireCommand([key, hashKey, 5, "GT"]).exec(client);
expect(res).toEqual([0]);
});
});

describe("LT", () => {
test("should set expiry only when the new expiry is less than current one", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 5]).exec(client);
const res = await new HExpireCommand([key, hashKey, 3, "LT"]).exec(client);
expect(res).toEqual([1]);
await new Promise((res) => setTimeout(res, 4000));
const res2 = await new HGetCommand([key, hashKey]).exec(client);
expect(res2).toEqual(null);
});

test("should not set expiry when the new expiry is not less than current one", async () => {
const key = newKey();
const hashKey = newKey();
const value = randomID();
await new HSetCommand([key, { [hashKey]: value }]).exec(client);
await new HExpireCommand([key, hashKey, 10]).exec(client);
const res = await new HExpireCommand([key, hashKey, 20, "LT"]).exec(client);
expect(res).toEqual([0]);
});
});

test("should return -2 if no such field exists in the provided hash key", async () => {
const key = newKey();
const hashKey = newKey();
const hashKey2 = newKey();
await new HSetCommand([key, { [hashKey]: 1 }]).exec(client);
const res = await new HExpireCommand([key, hashKey2, 1]).exec(client);
expect(res).toEqual([-2]);
});

test("should return results for multiple fields in order", async () => {
const key = newKey();
const hashKey1 = newKey();
const hashKey2 = newKey();
const hashKey3 = newKey();
const value1 = randomID();
const value2 = randomID();

await new HSetCommand([key, { [hashKey1]: value1, [hashKey2]: value2 }]).exec(client);

// Set expiry for the first field
await new HExpireCommand([key, hashKey1, 1]).exec(client);

// Pass both fields to HExpireCommand
const res = await new HExpireCommand([key, [hashKey1, hashKey2, hashKey3], 1, "NX"]).exec(client);

// Expect the results in order: hashKey1 already has expiry, hashKey2 does not
expect(res).toEqual([0, 1, -2]);

// Wait for the expiry to take effect
await new Promise((res) => setTimeout(res, 2000));

// Verify that hashKey1 is expired
const res1 = await new HGetCommand([key, hashKey1]).exec(client);
expect(res1).toEqual(null);

// Verify that hashKey2 is expired
const res2 = await new HGetCommand([key, hashKey2]).exec(client);
expect(res2).toEqual(null);
});
30 changes: 30 additions & 0 deletions pkg/commands/hexpire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { CommandOptions } from "./command";
import { Command } from "./command";

type HExpireOptions = "NX" | "nx" | "XX" | "xx" | "GT" | "gt" | "LT" | "lt";
export class HExpireCommand extends Command<(-2 | 0 | 1 | 2)[], (-2 | 0 | 1 | 2)[]> {
constructor(
cmd: [
key: string,
fields: (string | number) | (string | number)[],
seconds: number,
option?: HExpireOptions,
],
opts?: CommandOptions<(-2 | 0 | 1 | 2)[], (-2 | 0 | 1 | 2)[]>
) {
const [key, fields, seconds, option] = cmd;
const fieldArray = Array.isArray(fields) ? fields : [fields];
super(
[
"hexpire",
key,
seconds,
...(option ? [option] : []),
"FIELDS",
fieldArray.length,
...fieldArray,
],
opts
);
}
}
1 change: 1 addition & 0 deletions pkg/commands/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export * from "./getrange";
export * from "./getset";
export * from "./hdel";
export * from "./hexists";
export * from "./hexpire";
export * from "./hget";
export * from "./hgetall";
export * from "./hincrby";
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { type GetRangeCommand } from "./getrange";
export { type GetSetCommand } from "./getset";
export { type HDelCommand } from "./hdel";
export { type HExistsCommand } from "./hexists";
export { type HExpireCommand } from "./hexpire";
export { type HGetCommand } from "./hget";
export { type HGetAllCommand } from "./hgetall";
export { type HIncrByCommand } from "./hincrby";
Expand Down
7 changes: 4 additions & 3 deletions pkg/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ describe("use all the things", () => {
.getset(newKey(), "hello")
.hdel(newKey(), "field")
.hexists(newKey(), "field")
.hexpire(newKey(), "field", 1)
.hget(newKey(), "field")
.hgetall(newKey())
.hincrby(newKey(), "field", 1)
Expand Down Expand Up @@ -252,7 +253,7 @@ describe("use all the things", () => {
.json.set(newKey(), "$", { hello: "world" });

const res = await p.exec();
expect(res.length).toEqual(124);
expect(res.length).toEqual(125);
});
});
describe("keep errors", () => {
Expand Down Expand Up @@ -296,11 +297,11 @@ describe("keep errors", () => {
expect(results[0].error).toBeUndefined();
expect(results[1].error).toBeUndefined();
expect(results[2].error).toBe("NOSCRIPT No matching script. Please use EVAL.");
expect(results[3].error).toBe("ERR invalid expire time");
expect(results[3].error).toBeUndefined();
expect(results[4].error).toBeUndefined();

expect(results[2].result).toBeUndefined();
expect(results[3].result).toBeUndefined();
expect(results[3].result).toBe(1);
expect(results[4].result).toBe(2);
});
});
7 changes: 7 additions & 0 deletions pkg/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Command, CommandOptions } from "./commands/command";
import { HExpireCommand } from "./commands/hexpire";
import { HRandFieldCommand } from "./commands/hrandfield";
import type {
ScoreMember,
Expand Down Expand Up @@ -591,6 +592,12 @@ export class Pipeline<TCommands extends Command<any, any>[] = []> {
hexists = (...args: CommandArgs<typeof HExistsCommand>) =>
this.chain(new HExistsCommand(args, this.commandOptions));

/**
* @see https://redis.io/commands/hexpire
*/
hexpire = (...args: CommandArgs<typeof HExpireCommand>) =>
this.chain(new HExpireCommand(args, this.commandOptions));

/**
* @see https://redis.io/commands/hget
*/
Expand Down
18 changes: 13 additions & 5 deletions pkg/redis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createAutoPipelineProxy } from "../pkg/auto-pipeline";
import { HExpireCommand } from "./commands/hexpire";
import type {
CommandOptions,
ScoreMember,
Expand Down Expand Up @@ -431,11 +432,12 @@ export class Redis {
* expect(arg1, "Hello World")
* ```
*/
createScript(script: string): Script;
createScript(script: string, opts: { readonly?: false }): Script;
createScript(script: string, opts: { readonly: true }): ScriptRO;
createScript(script: string, opts?: { readonly?: boolean }): Script | ScriptRO {
return opts?.readonly ? new ScriptRO(this, script) : new Script(this, script);

createScript<TResult = unknown, TReadonly extends boolean = false>(
script: string,
opts?: { readonly?: TReadonly }
): TReadonly extends true ? ScriptRO<TResult> : Script<TResult> {
return opts?.readonly ? (new ScriptRO(this, script) as any) : (new Script(this, script) as any);
}

/**
Expand Down Expand Up @@ -709,6 +711,12 @@ export class Redis {
hexists = (...args: CommandArgs<typeof HExistsCommand>) =>
new HExistsCommand(args, this.opts).exec(this.client);

/**
* @see https://redis.io/commands/hexpire
*/
hexpire = (...args: CommandArgs<typeof HExpireCommand>) =>
new HExpireCommand(args, this.opts).exec(this.client);

/**
* @see https://redis.io/commands/hget
*/
Expand Down
Loading