* Execute an operation with exclusive access per key * Operations for the same key are serialized (run one at a time) * Operations for different keys can run concurrently
(key: K, operation: () => Promise<T>)
| 23 | * Operations for different keys can run concurrently |
| 24 | */ |
| 25 | async withLock<T>(key: K, operation: () => Promise<T>): Promise<T> { |
| 26 | // Chain onto existing lock (or resolved promise if none) |
| 27 | const previousLock = this.locks.get(key) ?? Promise.resolve(); |
| 28 | |
| 29 | let releaseLock: () => void; |
| 30 | const lockPromise = new Promise<void>((resolve) => { |
| 31 | releaseLock = resolve; |
| 32 | }); |
| 33 | |
| 34 | // ATOMIC: set our lock BEFORE awaiting previous |
| 35 | // This prevents the TOCTOU race where multiple callers see the same |
| 36 | // existing lock, all await it, then all proceed concurrently |
| 37 | this.locks.set(key, lockPromise); |
| 38 | |
| 39 | try { |
| 40 | await previousLock; // Wait for previous operation |
| 41 | return await operation(); |
| 42 | } finally { |
| 43 | releaseLock!(); |
| 44 | if (this.locks.get(key) === lockPromise) { |
| 45 | this.locks.delete(key); |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | } |
no test coverage detected