11import type { ResourceAcquire , ResourceRelease } from '@matrixai/resources' ;
2- import type { Lockable , ToString , LockRequest } from './types' ;
2+ import type {
3+ ToString ,
4+ Lockable ,
5+ MultiLockRequest ,
6+ MultiLockAcquire ,
7+ MultiLockAcquired ,
8+ } from './types' ;
39import { withF , withG } from '@matrixai/resources' ;
410import { ErrorAsyncLocksLockBoxConflict } from './errors' ;
511
6- class LockBox < L extends Lockable > implements Lockable {
12+ class LockBox < L extends Lockable = Lockable > implements Lockable {
713 protected _locks : Map < string , L > = new Map ( ) ;
814
9- public lock ( ...requests : Array < LockRequest < L > > ) : ResourceAcquire < LockBox < L > > {
15+ public lock (
16+ ...requests : Array < MultiLockRequest < L > >
17+ ) : ResourceAcquire < LockBox < L > > {
1018 return async ( ) => {
1119 // Convert to strings
1220 // This creates a copy of the requests
@@ -26,42 +34,48 @@ class LockBox<L extends Lockable> implements Lockable {
2634 ( [ key ] , i , arr ) => i === 0 || key !== arr [ i - 1 ] [ 0 ] ,
2735 ) ;
2836 const locks : Array < [ string , ResourceRelease , L ] > = [ ] ;
29- for ( const [ key , LockConstructor , ...lockingParams ] of requests_ ) {
30- let lock = this . _locks . get ( key ) ;
31- if ( lock == null ) {
32- lock = new LockConstructor ( ) ;
33- this . _locks . set ( key , lock ) ;
34- } else {
35- // It is possible to swap the lock class, but only after the lock key is released
36- if ( ! ( lock instanceof LockConstructor ) ) {
37- throw new ErrorAsyncLocksLockBoxConflict (
38- `Lock ${ key } is already locked with class ${ lock . constructor . name } , which conflicts with class ${ LockConstructor . name } ` ,
39- ) ;
37+ try {
38+ for ( const [ key , LockConstructor , ...lockingParams ] of requests_ ) {
39+ let lock = this . _locks . get ( key ) ;
40+ if ( lock == null ) {
41+ lock = new LockConstructor ( ) ;
42+ this . _locks . set ( key , lock ) ;
43+ } else {
44+ // It is possible to swap the lock class, but only after the lock key is released
45+ if ( ! ( lock instanceof LockConstructor ) ) {
46+ throw new ErrorAsyncLocksLockBoxConflict (
47+ `Lock ${ key } is already locked with class ${ lock . constructor . name } , which conflicts with class ${ LockConstructor . name } ` ,
48+ ) ;
49+ }
4050 }
51+ const lockAcquire = lock . lock ( ...lockingParams ) ;
52+ const [ lockRelease ] = await lockAcquire ( ) ;
53+ locks . push ( [ key , lockRelease , lock ] ) ;
4154 }
42- const lockAcquire = lock . lock ( ...lockingParams ) ;
43- let lockRelease : ResourceRelease ;
44- try {
45- [ lockRelease ] = await lockAcquire ( ) ;
46- } catch ( e ) {
47- // Release all intermediate locks in reverse order
48- locks . reverse ( ) ;
49- for ( const [ key , lockRelease , lock ] of locks ) {
50- await lockRelease ( ) ;
51- if ( ! lock . isLocked ( ) ) {
52- this . _locks . delete ( key ) ;
53- }
55+ } catch ( e ) {
56+ // Release all intermediate locks in reverse order
57+ locks . reverse ( ) ;
58+ for ( const [ key , lockRelease , lock ] of locks ) {
59+ await lockRelease ( ) ;
60+ // If it is still locked, then it is held by a different context
61+ // only delete if no contexts are locking the lock
62+ if ( ! lock . isLocked ( ) ) {
63+ this . _locks . delete ( key ) ;
5464 }
55- throw e ;
5665 }
57- locks . push ( [ key , lockRelease , lock ] ) ;
66+ throw e ;
5867 }
68+ let released = false ;
5969 return [
6070 async ( ) => {
71+ if ( released ) return ;
72+ released = true ;
6173 // Release all locks in reverse order
6274 locks . reverse ( ) ;
6375 for ( const [ key , lockRelease , lock ] of locks ) {
6476 await lockRelease ( ) ;
77+ // If it is still locked, then it is held by a different context
78+ // only delete if no contexts are locking the lock
6579 if ( ! lock . isLocked ( ) ) {
6680 this . _locks . delete ( key ) ;
6781 }
@@ -72,6 +86,76 @@ class LockBox<L extends Lockable> implements Lockable {
7286 } ;
7387 }
7488
89+ public lockMulti (
90+ ...requests : Array < MultiLockRequest < L > >
91+ ) : Array < MultiLockAcquire < L > > {
92+ // Convert to strings
93+ // This creates a copy of the requests
94+ let requests_ : Array <
95+ [ string , ToString , new ( ) => L , ...Parameters < L [ 'lock' ] > ]
96+ > = requests . map ( ( [ key , ...rest ] ) =>
97+ typeof key === 'string'
98+ ? [ key , key , ...rest ]
99+ : [ key . toString ( ) , key , ...rest ] ,
100+ ) ;
101+ // Sort to ensure lock hierarchy
102+ requests_ . sort ( ( [ key1 ] , [ key2 ] ) => {
103+ // Deterministic string comparison according to 16-bit code units
104+ if ( key1 < key2 ) return - 1 ;
105+ if ( key1 > key2 ) return 1 ;
106+ return 0 ;
107+ } ) ;
108+ // Avoid duplicate locking
109+ requests_ = requests_ . filter (
110+ ( [ key ] , i , arr ) => i === 0 || key !== arr [ i - 1 ] [ 0 ] ,
111+ ) ;
112+ const lockAcquires : Array < MultiLockAcquire < L > > = [ ] ;
113+ for ( const [ key , keyOrig , LockConstructor , ...lockingParams ] of requests_ ) {
114+ const lockAcquire : ResourceAcquire < L > = async ( ) => {
115+ let lock = this . _locks . get ( key ) ;
116+ let lockRelease : ResourceRelease ;
117+ try {
118+ if ( lock == null ) {
119+ lock = new LockConstructor ( ) ;
120+ this . _locks . set ( key , lock ) ;
121+ } else {
122+ // It is possible to swap the lock class, but only after the lock key is released
123+ if ( ! ( lock instanceof LockConstructor ) ) {
124+ throw new ErrorAsyncLocksLockBoxConflict (
125+ `Lock ${ key } is already locked with class ${ lock . constructor . name } , which conflicts with class ${ LockConstructor . name } ` ,
126+ ) ;
127+ }
128+ }
129+ const lockAcquire = lock . lock ( ...lockingParams ) ;
130+ [ lockRelease ] = await lockAcquire ( ) ;
131+ } catch ( e ) {
132+ // If it is still locked, then it is held by a different context
133+ // only delete if no contexts are locking the lock
134+ if ( ! lock ! . isLocked ( ) ) {
135+ this . _locks . delete ( key ) ;
136+ }
137+ throw e ;
138+ }
139+ let released = false ;
140+ return [
141+ async ( ) => {
142+ if ( released ) return ;
143+ released = true ;
144+ await lockRelease ( ) ;
145+ // If it is still locked, then it is held by a different context
146+ // only delete if no contexts are locking the lock
147+ if ( ! lock ! . isLocked ( ) ) {
148+ this . _locks . delete ( key ) ;
149+ }
150+ } ,
151+ lock ,
152+ ] ;
153+ } ;
154+ lockAcquires . push ( [ keyOrig , lockAcquire , ...lockingParams ] ) ;
155+ }
156+ return lockAcquires ;
157+ }
158+
75159 get locks ( ) : ReadonlyMap < string , L > {
76160 return this . _locks ;
77161 }
@@ -116,31 +200,88 @@ class LockBox<L extends Lockable> implements Lockable {
116200
117201 public async withF < T > (
118202 ...params : [
119- ...requests : Array < LockRequest < L > > ,
203+ ...requests : Array < MultiLockRequest < L > > ,
120204 f : ( lockBox : LockBox < L > ) => Promise < T > ,
121205 ]
122206 ) : Promise < T > {
123207 const f = params . pop ( ) as ( lockBox : LockBox < L > ) => Promise < T > ;
124208 return withF (
125- [ this . lock ( ...( params as Array < LockRequest < L > > ) ) ] ,
209+ [ this . lock ( ...( params as Array < MultiLockRequest < L > > ) ) ] ,
126210 ( [ lockBox ] ) => f ( lockBox ) ,
127211 ) ;
128212 }
129213
214+ public async withMultiF < T > (
215+ ...params : [
216+ ...requests : Array < MultiLockRequest < L > > ,
217+ f : ( multiLocks : Array < MultiLockAcquired < L > > ) => Promise < T > ,
218+ ]
219+ ) : Promise < T > {
220+ const f = params . pop ( ) as (
221+ multiLocks : Array < MultiLockAcquired < L > > ,
222+ ) => Promise < T > ;
223+ const lockAcquires = this . lockMulti (
224+ ...( params as Array < MultiLockRequest < L > > ) ,
225+ ) ;
226+
227+ const lockAcquires_ : Array < ResourceAcquire < MultiLockAcquired < L > > > =
228+ lockAcquires . map (
229+ ( [ key , lockAcquire , ...lockingParams ] ) =>
230+ ( ...r ) =>
231+ lockAcquire ( ...r ) . then (
232+ ( [ lockRelease , lock ] ) =>
233+ [ lockRelease , [ key , lock , ...lockingParams ] ] as [
234+ ResourceRelease ,
235+ MultiLockAcquired < L > ,
236+ ] ,
237+ ) ,
238+ ) ;
239+ return withF ( lockAcquires_ , f ) ;
240+ }
241+
130242 public withG < T , TReturn , TNext > (
131243 ...params : [
132- ...requests : Array < LockRequest < L > > ,
244+ ...requests : Array < MultiLockRequest < L > > ,
133245 g : ( lockBox : LockBox < L > ) => AsyncGenerator < T , TReturn , TNext > ,
134246 ]
135247 ) : AsyncGenerator < T , TReturn , TNext > {
136248 const g = params . pop ( ) as (
137249 lockBox : LockBox < L > ,
138250 ) => AsyncGenerator < T , TReturn , TNext > ;
139251 return withG (
140- [ this . lock ( ...( params as Array < LockRequest < L > > ) ) ] ,
252+ [ this . lock ( ...( params as Array < MultiLockRequest < L > > ) ) ] ,
141253 ( [ lockBox ] ) => g ( lockBox ) ,
142254 ) ;
143255 }
256+
257+ public withMultiG < T , TReturn , TNext > (
258+ ...params : [
259+ ...requests : Array < MultiLockRequest < L > > ,
260+ g : (
261+ multiLocks : Array < MultiLockAcquired < L > > ,
262+ ) => AsyncGenerator < T , TReturn , TNext > ,
263+ ]
264+ ) {
265+ const g = params . pop ( ) as (
266+ multiLocks : Array < MultiLockAcquired < L > > ,
267+ ) => AsyncGenerator < T , TReturn , TNext > ;
268+ const lockAcquires = this . lockMulti (
269+ ...( params as Array < MultiLockRequest < L > > ) ,
270+ ) ;
271+ const lockAcquires_ : Array < ResourceAcquire < MultiLockAcquired < L > > > =
272+ lockAcquires . map (
273+ ( [ key , lockAcquire , ...lockingParams ] ) =>
274+ ( ...r ) =>
275+ lockAcquire ( ...r ) . then (
276+ ( [ lockRelease , lock ] ) =>
277+ [ lockRelease , [ key , lock , ...lockingParams ] ] as [
278+ ResourceRelease ,
279+ MultiLockAcquired < L > ,
280+ ] ,
281+ ) ,
282+ ) ;
283+ return withG ( lockAcquires_ , g ) ;
284+ }
144285}
145286
146287export default LockBox ;
0 commit comments