import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier'; import type { ReadWriteLocker } from '../../../../src/util/locking/ReadWriteLocker'; import { WrappedExpiringReadWriteLocker } from '../../../../src/util/locking/WrappedExpiringReadWriteLocker'; jest.useFakeTimers(); describe('A WrappedExpiringReadWriteLocker', (): void => { const identifier = { path: 'path' }; let syncCb: () => string; let asyncCb: () => Promise; let wrappedLocker: ReadWriteLocker; let locker: WrappedExpiringReadWriteLocker; const expiration = 1000; beforeEach(async(): Promise => { wrappedLocker = { withReadLock: jest.fn(async(id: ResourceIdentifier, whileLocked: () => T | Promise): Promise => whileLocked()), withWriteLock: jest.fn(async(id: ResourceIdentifier, whileLocked: () => T | Promise): Promise => whileLocked()), }; syncCb = jest.fn((): string => 'sync'); asyncCb = jest.fn(async(): Promise => new Promise((resolve): void => { setImmediate((): void => resolve('async')); })); locker = new WrappedExpiringReadWriteLocker(wrappedLocker, expiration); }); it('calls the wrapped locker for locking.', async(): Promise => { let prom = locker.withReadLock(identifier, syncCb); await expect(prom).resolves.toBe('sync'); expect(wrappedLocker.withReadLock).toHaveBeenCalledTimes(1); expect((wrappedLocker.withReadLock as jest.Mock).mock.calls[0][0]).toBe(identifier); prom = locker.withWriteLock(identifier, syncCb); await expect(prom).resolves.toBe('sync'); expect(wrappedLocker.withWriteLock).toHaveBeenCalledTimes(1); expect((wrappedLocker.withWriteLock as jest.Mock).mock.calls[0][0]).toBe(identifier); }); it('calls the functions that need to be locked through the wrapped locker.', async(): Promise => { let prom = locker.withReadLock(identifier, syncCb); await expect(prom).resolves.toBe('sync'); expect(syncCb).toHaveBeenCalledTimes(1); prom = locker.withReadLock(identifier, asyncCb); // Execute promise (without triggering timeout) jest.advanceTimersByTime(100); await expect(prom).resolves.toBe('async'); expect(asyncCb).toHaveBeenCalledTimes(1); }); it('throws an error if the locked function resolves too slow.', async(): Promise => { async function slowCb(): Promise { return new Promise((resolve): any => setTimeout(resolve, 5000)); } const prom = locker.withReadLock(identifier, slowCb); jest.advanceTimersByTime(1000); await expect(prom).rejects.toThrow(`Lock expired after ${expiration}ms on ${identifier.path}`); }); it('can reset the timer within the locked function.', async(): Promise => { async function refreshCb(maintainLock: () => void): Promise { return new Promise((resolve): any => { setTimeout(maintainLock, 750); setTimeout((): void => resolve('refresh'), 1500); }); } const prom = locker.withReadLock(identifier, refreshCb); jest.advanceTimersByTime(1500); await expect(prom).resolves.toBe('refresh'); }); it('can still error after resetting the timer.', async(): Promise => { async function refreshCb(maintainLock: () => void): Promise { return new Promise((resolve): any => { setTimeout(maintainLock, 750); setTimeout(maintainLock, 1500); setTimeout(resolve, 5000); }); } const prom = locker.withReadLock(identifier, refreshCb); jest.advanceTimersByTime(5000); await expect(prom).rejects.toThrow(`Lock expired after ${expiration}ms on ${identifier.path}`); }); });