diff --git a/src/LockBox.ts b/src/LockBox.ts index 6425984..e001d19 100644 --- a/src/LockBox.ts +++ b/src/LockBox.ts @@ -1,12 +1,18 @@ import type { ResourceAcquire, ResourceRelease } from '@matrixai/resources'; -import type { Lockable, ToString, LockRequest } from './types'; +import type { + ToString, + Lockable, + MultiLockRequest, + MultiLockAcquire, + MultiLockAcquired +} from './types'; import { withF, withG } from '@matrixai/resources'; import { ErrorAsyncLocksLockBoxConflict } from './errors'; class LockBox implements Lockable { protected _locks: Map = new Map(); - public lock(...requests: Array>): ResourceAcquire> { + public lock(...requests: Array>): ResourceAcquire> { return async () => { // Convert to strings // This creates a copy of the requests @@ -79,8 +85,8 @@ class LockBox implements Lockable { } public lockMulti( - ...requests: Array> - ): Array<[ToString, ResourceAcquire]> { + ...requests: Array> + ): Array> { // Convert to strings // This creates a copy of the requests let requests_: Array< @@ -101,51 +107,49 @@ class LockBox implements Lockable { requests_ = requests_.filter( ([key], i, arr) => i === 0 || key !== arr[i - 1][0], ); - const lockAcquires: Array<[ToString, ResourceAcquire]> = []; + const lockAcquires: Array> = []; for (const [key, keyOrig, LockConstructor, ...lockingParams] of requests_) { - lockAcquires.push([ - keyOrig, - async () => { - let lock = this._locks.get(key); - let lockRelease: ResourceRelease; - try { - if (lock == null) { - lock = new LockConstructor(); - this._locks.set(key, lock); - } else { - // It is possible to swap the lock class, but only after the lock key is released - if (!(lock instanceof LockConstructor)) { - throw new ErrorAsyncLocksLockBoxConflict( - `Lock ${key} is already locked with class ${lock.constructor.name}, which conflicts with class ${LockConstructor.name}`, - ); - } + const lockAcquire: ResourceAcquire = async () => { + let lock = this._locks.get(key); + let lockRelease: ResourceRelease; + try { + if (lock == null) { + lock = new LockConstructor(); + this._locks.set(key, lock); + } else { + // It is possible to swap the lock class, but only after the lock key is released + if (!(lock instanceof LockConstructor)) { + throw new ErrorAsyncLocksLockBoxConflict( + `Lock ${key} is already locked with class ${lock.constructor.name}, which conflicts with class ${LockConstructor.name}`, + ); } - const lockAcquire = lock.lock(...lockingParams); - [lockRelease] = await lockAcquire(); - } catch (e) { + } + const lockAcquire = lock.lock(...lockingParams); + [lockRelease] = await lockAcquire(); + } catch (e) { + // If it is still locked, then it is held by a different context + // only delete if no contexts are locking the lock + if (!lock!.isLocked()) { + this._locks.delete(key); + } + throw e; + } + let released = false; + return [ + async () => { + if (released) return; + released = true; + await lockRelease(); // If it is still locked, then it is held by a different context // only delete if no contexts are locking the lock if (!lock!.isLocked()) { this._locks.delete(key); } - throw e; - } - let released = false; - return [ - async () => { - if (released) return; - released = true; - await lockRelease(); - // If it is still locked, then it is held by a different context - // only delete if no contexts are locking the lock - if (!lock!.isLocked()) { - this._locks.delete(key); - } - }, - lock, - ]; - }, - ]); + }, + lock, + ]; + }; + lockAcquires.push([keyOrig, lockAcquire, ...lockingParams]); } return lockAcquires; } @@ -194,32 +198,33 @@ class LockBox implements Lockable { public async withF( ...params: [ - ...requests: Array>, + ...requests: Array>, f: (lockBox: LockBox) => Promise, ] ): Promise { const f = params.pop() as (lockBox: LockBox) => Promise; return withF( - [this.lock(...(params as Array>))], + [this.lock(...(params as Array>))], ([lockBox]) => f(lockBox), ); } public async withMultiF( ...params: [ - ...requests: Array>, - f: (multiLocks: Array<[ToString, L]>) => Promise, + ...requests: Array>, + f: (multiLocks: Array>) => Promise, ] ): Promise { - const f = params.pop() as (multiLocks: Array<[ToString, L]>) => Promise; - const lockAcquires = this.lockMulti(...(params as Array>)); - const lockAcquires_: Array> = + const f = params.pop() as (multiLocks: Array>) => Promise; + const lockAcquires = this.lockMulti(...(params as Array>)); + + const lockAcquires_: Array>> = lockAcquires.map( - ([key, lockAcquire]) => + ([key, lockAcquire, ...lockingParams]) => (...r) => lockAcquire(...r).then( ([lockRelease, lock]) => - [lockRelease, [key, lock]] as [ResourceRelease, [ToString, L]], + [lockRelease, [key, lock, ...lockingParams]] as [ResourceRelease, MultiLockAcquired], ), ); return withF(lockAcquires_, f); @@ -227,7 +232,7 @@ class LockBox implements Lockable { public withG( ...params: [ - ...requests: Array>, + ...requests: Array>, g: (lockBox: LockBox) => AsyncGenerator, ] ): AsyncGenerator { @@ -235,30 +240,30 @@ class LockBox implements Lockable { lockBox: LockBox, ) => AsyncGenerator; return withG( - [this.lock(...(params as Array>))], + [this.lock(...(params as Array>))], ([lockBox]) => g(lockBox), ); } public withMultiG( ...params: [ - ...requests: Array>, + ...requests: Array>, g: ( - multiLocks: Array<[ToString, L]>, + multiLocks: Array>, ) => AsyncGenerator, ] ) { const g = params.pop() as ( - multiLocks: Array<[ToString, L]>, + multiLocks: Array>, ) => AsyncGenerator; - const lockAcquires = this.lockMulti(...(params as Array>)); - const lockAcquires_: Array> = + const lockAcquires = this.lockMulti(...(params as Array>)); + const lockAcquires_: Array>> = lockAcquires.map( - ([key, lockAcquire]) => + ([key, lockAcquire, ...lockingParams]) => (...r) => lockAcquire(...r).then( ([lockRelease, lock]) => - [lockRelease, [key, lock]] as [ResourceRelease, [ToString, L]], + [lockRelease, [key, lock, ...lockingParams]] as [ResourceRelease, MultiLockAcquired], ), ); return withG(lockAcquires_, g); diff --git a/src/types.ts b/src/types.ts index a706efe..8bab0f7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,10 +23,22 @@ interface Lockable { ): AsyncGenerator; } -type LockRequest = [ +type MultiLockRequest = [ key: ToString, lockConstructor: new () => L, ...lockingParams: Parameters, ]; -export type { POJO, ToString, Lockable, LockRequest }; +type MultiLockAcquire = [ + key: ToString, + lockAcquire: ResourceAcquire, + ...lockingParams: Parameters, +]; + +type MultiLockAcquired = [ + key: ToString, + lock: L, + ...lockingParams: Parameters, +]; + +export type { POJO, ToString, Lockable, MultiLockRequest, MultiLockAcquire, MultiLockAcquired }; diff --git a/tests/LockBox.test.ts b/tests/LockBox.test.ts index b03b175..f79fece 100644 --- a/tests/LockBox.test.ts +++ b/tests/LockBox.test.ts @@ -1,5 +1,5 @@ import type { ResourceRelease } from '@matrixai/resources'; -import type { LockRequest } from '@/types'; +import type { MultiLockRequest } from '@/types'; import { withF, withG } from '@matrixai/resources'; import LockBox from '@/LockBox'; import Lock from '@/Lock'; @@ -339,7 +339,7 @@ describe(LockBox.name, () => { test('can map keys to LockBox locks', async () => { const lockBox = new LockBox(); const keys = ['1', '2', '3', '4']; - const locks: Array> = keys.map((key) => [ + const locks: Array> = keys.map((key) => [ key, RWLockWriter, 'write',