diff --git a/packages/homeserver/src/mutex/Mutex.ts b/packages/homeserver/src/mutex/Mutex.ts new file mode 100644 index 00000000..c1b7222f --- /dev/null +++ b/packages/homeserver/src/mutex/Mutex.ts @@ -0,0 +1,23 @@ +export class Mutex { + private map: Map = new Map(); + public async request(scope: string) { + if (this.map.has(scope)) { + return false; + } + + const lock = new Lock(this, scope, () => this.map.delete(scope)); + this.map.set(scope, true); + return lock; + } +} + +export class Lock { + constructor( + protected m: Mutex, + public scope: string, + private unlock: () => void, + ) {} + public async release() { + this.unlock(); + } +} diff --git a/packages/homeserver/src/mutex/mutext.spec.ts b/packages/homeserver/src/mutex/mutext.spec.ts new file mode 100644 index 00000000..f5367d15 --- /dev/null +++ b/packages/homeserver/src/mutex/mutext.spec.ts @@ -0,0 +1,41 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { Mutex, type Lock } from "./Mutex"; + +describe("Mutex", () => { + let mutex: Mutex; + let lock: Lock | false; + + beforeEach(() => { + mutex = new Mutex(); + }); + + afterEach(() => { + if (lock) { + lock.release(); + } + }); + + it("should grant a lock if the scope is not already locked", async () => { + const scope = "test-scope"; + lock = await mutex.request(scope); + expect(lock).toBeTruthy(); + }); + + it("should not grant a lock if the scope is already locked", async () => { + const scope = "test-scope"; + await mutex.request(scope); + const secondLock = await mutex.request(scope); + expect(secondLock).toBeFalsy(); + }); + + it("should release a lock and allow re-locking the same scope", async () => { + const scope = "test-scope"; + lock = await mutex.request(scope); + expect(lock).not.toBeFalse(); + + await (lock as Lock).release(); + + const newLock = await mutex.request(scope); + expect(newLock).toBeTruthy(); + }); +}); diff --git a/packages/homeserver/src/plugins/mutex.ts b/packages/homeserver/src/plugins/mutex.ts new file mode 100644 index 00000000..f89c9f17 --- /dev/null +++ b/packages/homeserver/src/plugins/mutex.ts @@ -0,0 +1,10 @@ +import Elysia, { type InferContext } from "elysia"; +import { Mutex } from "../mutex/Mutex"; + +export const routerWithMutex = new Elysia().decorate("mutex", new Mutex()); + +export const isMutexContext = ( + context: T, +): context is T & Context => "mutex" in context; + +type Context = InferContext;