From 3994f349f4412e66f7ed6825fd04f8aad5c19f7d Mon Sep 17 00:00:00 2001 From: robonen Date: Fri, 9 May 2025 13:12:07 +0700 Subject: [PATCH] feat(packages/stdlib): add execute method for SyncMutex --- packages/stdlib/src/sync/mutex/index.test.ts | 51 +++++++++++++++++++- packages/stdlib/src/sync/mutex/index.ts | 20 +++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/stdlib/src/sync/mutex/index.test.ts b/packages/stdlib/src/sync/mutex/index.test.ts index c50ae0b..3b97fc0 100644 --- a/packages/stdlib/src/sync/mutex/index.test.ts +++ b/packages/stdlib/src/sync/mutex/index.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { SyncMutex } from '.'; describe('SyncMutex', () => { @@ -14,24 +14,28 @@ describe('SyncMutex', () => { it('lock the mutex', () => { mutex.lock(); + expect(mutex.isLocked).toBe(true); }); it('remain locked when locked multiple times', () => { mutex.lock(); mutex.lock(); + expect(mutex.isLocked).toBe(true); }); it('unlock a locked mutex', () => { mutex.lock(); mutex.unlock(); + expect(mutex.isLocked).toBe(false); }); it('remain unlocked when unlocked multiple times', () => { mutex.unlock(); mutex.unlock(); + expect(mutex.isLocked).toBe(false); }); @@ -42,4 +46,49 @@ describe('SyncMutex', () => { mutex.unlock(); expect(mutex.isLocked).toBe(false); }); + + it('execute a callback when unlocked', async () => { + const callback = vi.fn(() => 'done'); + const result = await mutex.execute(callback); + + expect(result).toBe('done'); + expect(callback).toHaveBeenCalled(); + }); + + it('execute a promise callback when unlocked', async () => { + const callback = vi.fn(() => Promise.resolve('done')); + const result = await mutex.execute(callback); + + expect(result).toBe('done'); + expect(callback).toHaveBeenCalled(); + }); + + it('execute concurrent callbacks only one at a time', async () => { + const callback = vi.fn(() => Promise.resolve('done')); + + const result = await Promise.all([ + mutex.execute(callback), + mutex.execute(callback), + mutex.execute(callback), + ]); + + expect(result).toEqual(['done', undefined, undefined]); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not execute a callback when locked', async () => { + const callback = vi.fn(() => 'done'); + mutex.lock(); + const result = await mutex.execute(callback); + + expect(result).toBeUndefined(); + expect(callback).not.toHaveBeenCalled(); + }); + + it('unlocks after executing a callback', async () => { + const callback = vi.fn(() => 'done'); + await mutex.execute(callback); + + expect(mutex.isLocked).toBe(false); + }); }); diff --git a/packages/stdlib/src/sync/mutex/index.ts b/packages/stdlib/src/sync/mutex/index.ts index acb1610..991bc19 100644 --- a/packages/stdlib/src/sync/mutex/index.ts +++ b/packages/stdlib/src/sync/mutex/index.ts @@ -1,3 +1,5 @@ +import type { MaybePromise } from "../../types"; + /** * @name SyncMutex * @category Utils @@ -10,10 +12,15 @@ * * mutex.unlock(); * + * const result = await mutex.execute(() => { + * // do something + * return Promise.resolve('done'); + * }); + * * @since 0.0.5 */ export class SyncMutex { - private state: boolean = false; + private state = false; public get isLocked() { return this.state; @@ -26,4 +33,15 @@ export class SyncMutex { public unlock() { this.state = false; } + + public async execute(callback: () => T) { + if (this.isLocked) + return; + + this.lock(); + const result = await callback(); + this.unlock(); + + return result; + } }