diff --git a/package.json b/package.json index 102a6b9..26b9d7d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "files": ["dist"], "scripts": { "build": "tsup", - "fmt": "pnpm biome format . --write && pnpm biome check . --apply-unsafe" + "test": "bun test src --coverage", + "fmt": "bunx @biomejs/biome check --apply ." }, "devDependencies": { "@biomejs/biome": "1.3.1", diff --git a/src/lock-manager.ts b/src/lock-manager.ts index 5b892e9..f6ec597 100644 --- a/src/lock-manager.ts +++ b/src/lock-manager.ts @@ -1,14 +1,7 @@ import { randomUUID } from "crypto"; import { Redis } from "@upstash/redis"; import { Lock } from "./lock"; -import { LockAcquireConfig } from "./types"; - -type LockManagerConfig = { - /** - * Upstash Redis client instance used for locking operations. - */ - redis: Redis; -}; +import type { LockAcquireConfig, LockManagerConfig } from "./types"; export class LockManager { private readonly redis: Redis; diff --git a/src/lock.test.ts b/src/lock.test.ts new file mode 100644 index 0000000..a94c8c0 --- /dev/null +++ b/src/lock.test.ts @@ -0,0 +1,96 @@ +import { expect, test } from "bun:test"; +import { Redis } from "@upstash/redis"; +import { LockManager } from "./lock-manager"; + +function getUniqueLockId() { + return `lock-test-${Math.random().toString(36).substr(2, 9)}`; +} + +test("lock created, extended, and released with defaults", async () => { + const lm = new LockManager({ + redis: Redis.fromEnv(), + }); + const id = getUniqueLockId(); + const lock = await lm.acquire({ id }); + + expect(lock.id).toBe(id); + expect(lock.status).toBe("ACQUIRED"); + const extended = await lock.extend(10000); + expect(extended).toBe(true); + const released = await lock.release(); + expect(released).toBe(true); + expect(lock.status).toBe("RELEASED"); +}); + +test("lock created, extended, and released with values", async () => { + const lm = new LockManager({ + redis: Redis.fromEnv(), + }); + + const id = getUniqueLockId(); + const lock = await lm.acquire({ + id, + lease: 5000, + retry: { + attempts: 1, + delay: 100, + }, + }); + + expect(lock.id).toBe(id); + expect(lock.status).toBe("ACQUIRED"); + const extended = await lock.extend(10000); + expect(extended).toBe(true); + const released = await lock.release(); + expect(released).toBe(true); + expect(lock.status).toBe("RELEASED"); +}); + +test("lock acquisition fails", async () => { + const lm = new LockManager({ + redis: Redis.fromEnv(), + }); + + const id = getUniqueLockId(); + const lockSuccess = await lm.acquire({ + id, + lease: 5000, + retry: { + attempts: 1, + delay: 100, + }, + }); + + expect(lockSuccess.status).toBe("ACQUIRED"); + + // Since the lock was already acquired, this should fail + const lockFail = await lm.acquire({ + id, + retry: { + attempts: 1, + delay: 100, + }, + }); + + expect(lockFail.status).toBe("FAILED"); +}); + +test("lock lease times out", async () => { + const lm = new LockManager({ + redis: Redis.fromEnv(), + }); + + const lock = await lm.acquire({ + id: "lock-test-3", + lease: 100, + retry: { + attempts: 1, + delay: 100, + }, + }); + + // Wait for the lock to expire + setTimeout(async () => { + expect(lock.status).toBe("RELEASED"); + }, 200); +}); diff --git a/src/lock.ts b/src/lock.ts index 24a22cd..6f91041 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -1,33 +1,4 @@ -import { Redis } from "@upstash/redis"; -import { LockStatus } from "./types"; - -type LockConfig = { - /** - * Upstash Redis client instance for locking operations. - */ - redis: Redis; - - /** - * Unique identifier associated with the lock. - */ - id: string; - - /** - * Current status of the lock (e.g., ACQUIRED, RELEASED). - */ - status: LockStatus; - - /** - * Duration (in ms) for which the lock should be held. - */ - lease: number; - - /** - * A unique value assigned when the lock is acquired. - * It's set to null if the lock isn't successfully acquired. - */ - UUID: string | null; -}; +import type { LockConfig, LockStatus } from "./types"; export class Lock { private readonly config: LockConfig; @@ -84,6 +55,10 @@ export class Lock { [this.config.id], [this.config.UUID, extendBy], ); + + if (extended === 1) { + this.config.lease += amt; + } return extended === 1; } diff --git a/src/types.ts b/src/types.ts index 0872993..2207831 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,12 @@ +import type { Redis } from "@upstash/redis"; + +export type LockManagerConfig = { + /** + * Upstash Redis client instance used for locking operations. + */ + redis: Redis; +}; + export type RetryConfig = { /** * The number of times to retry acquiring the lock before giving up. @@ -30,4 +39,32 @@ export type LockAcquireConfig = { retry?: RetryConfig; }; +export type LockConfig = { + /** + * Upstash Redis client instance for locking operations. + */ + redis: Redis; + + /** + * Unique identifier associated with the lock. + */ + id: string; + + /** + * Current status of the lock (e.g., ACQUIRED, RELEASED). + */ + status: LockStatus; + + /** + * Duration (in ms) for which the lock should be held. + */ + lease: number; + + /** + * A unique value assigned when the lock is acquired. + * It's set to null if the lock isn't successfully acquired. + */ + UUID: string | null; +}; + export type LockStatus = "ACQUIRED" | "RELEASED" | "FAILED";