├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── Logger.js ├── errors.js ├── factories │ ├── createMutex.js │ └── index.js ├── index.js └── types.js └── test ├── .eslintrc └── mutual-exclusion └── factories └── createMutex.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "istanbul" 6 | ] 7 | } 8 | }, 9 | "plugins": [ 10 | "transform-export-default-name", 11 | "@babel/transform-flow-strip-types" 12 | ], 13 | "presets": [ 14 | [ 15 | "@babel/env", 16 | { 17 | "corejs": "core-js@3", 18 | "targets": { 19 | "node": "12" 20 | }, 21 | "useBuiltIns": "usage" 22 | } 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/mutual-exclusion/c4f8e46615674a9521f93b3ed92ade87150b823b/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/flowtype" 5 | ], 6 | "root": true, 7 | "rules": { 8 | "class-methods-use-this": 0, 9 | "fp/no-class": 0, 10 | "fp/no-events": 0, 11 | "fp/no-this": 0, 12 | "import/no-cycle": 0, 13 | "no-continue": 0, 14 | "no-restricted-syntax": 0, 15 | "no-unused-expressions": [ 16 | 2, 17 | { 18 | "allowTaggedTemplates": true 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/test/.* 3 | /dist/.* 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintignore 9 | !.eslintrc 10 | !.flowconfig 11 | !.gitignore 12 | !.npmignore 13 | !.npmrc 14 | !.travis.yml 15 | !.README 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | coverage 3 | .* 4 | *.log 5 | !.flowconfig 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 12 5 | script: 6 | - npm run build 7 | - npm run test 8 | - npm run lint 9 | - NODE_ENV=test nyc --silent ava --serial 10 | - nyc report --reporter=text-lcov | coveralls 11 | - nyc check-coverage --lines 50 12 | after_success: 13 | - npm run build 14 | - semantic-release 15 | notifications: 16 | email: false 17 | sudo: false 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mutual-exclusion (mutex) 2 | 3 | [![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/mutual-exclusion?style=flat-square)](https://gitspo.com/mentions/gajus/mutual-exclusion) 4 | [![Travis build status](http://img.shields.io/travis/gajus/mutual-exclusion/master.svg?style=flat-square)](https://travis-ci.org/gajus/mutual-exclusion) 5 | [![Coveralls](https://img.shields.io/coveralls/gajus/mutual-exclusion.svg?style=flat-square)](https://coveralls.io/github/gajus/mutual-exclusion) 6 | [![NPM version](http://img.shields.io/npm/v/mutual-exclusion.svg?style=flat-square)](https://www.npmjs.org/package/mutual-exclusion) 7 | [![Canonical Code Style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/kuizinas.svg?style=social&label=Follow)](https://twitter.com/kuizinas) 9 | 10 | Mutual Exclusion ([mutex](https://en.wikipedia.org/wiki/Mutual_exclusion)) object for JavaScript. 11 | 12 | * [Motivation](#motivation) 13 | * [API](#api) 14 | * [Configuration](#configuration) 15 | * [Usage examples](#usage-examples) 16 | * [Image server example](#image-server-example) 17 | * [Related libraries](#related-libraries) 18 | 19 | ## Motivation 20 | 21 | Promised based mutex allows to sequentially perform the same asynchronous operation. A typical use case example is checking if a resource exists and reading/ creating resource as an atomic asynchronous operation. 22 | 23 | Suppose that you have a HTTP service that upon request downloads and serves an image. A naive implementation might look something like this: 24 | 25 | ```js 26 | router.use('/images/:uid', async (incomingMessage, serverResponse) => { 27 | const uid = incomingMessage.params.uid; 28 | 29 | const temporaryDownloadPath = path.resolve( 30 | downloadDirectoryPath, 31 | uid, 32 | ); 33 | 34 | const temporaryFileExists = await fs.pathExists(temporaryDownloadPath); 35 | 36 | if (!temporaryFileExists) { 37 | try { 38 | await pipeline( 39 | // Some external storage engine. 40 | storage.createReadStream('images/' + uid), 41 | fs.createWriteStream(temporaryDownloadPath), 42 | ); 43 | } catch (error) { 44 | await fs.remove(temporaryDownloadPath); 45 | 46 | throw error; 47 | } 48 | } 49 | 50 | fs 51 | .createReadStream(temporaryDownloadPath) 52 | .pipe(serverResponse); 53 | }); 54 | 55 | ``` 56 | 57 | In the above example, if two requests are made at near the same time, then both of them will identify that `temporaryFileExists` is `false` and at least one of them will fail. Mutex solves this problem by limiting concurrent execution of several asynchronous operations (see [Image server example](#image-server-example)). 58 | 59 | ## API 60 | 61 | ```js 62 | import { 63 | createMutex, 64 | HoldTimeoutError, 65 | WaitTimeoutError,, 66 | } from 'mutual-exclusion'; 67 | import type { 68 | LockConfigurationInputType, 69 | MutexConfigurationInputType, 70 | MutexType, 71 | } from 'mutual-exclusion'; 72 | 73 | /** 74 | * Thrown in case of `holdTimeout`. 75 | */ 76 | HoldTimeoutError; 77 | 78 | /** 79 | * Thrown in case of `waitTimeout`. 80 | */ 81 | WaitTimeoutError; 82 | 83 | const mutex: MutexType = createMutex(mutexConfigurationInput: MutexConfigurationInputType); 84 | 85 | (async () => { 86 | // Lock is acquired. 87 | await mutex.lock(async () => { 88 | // Perform whatever operation that requires locking. 89 | 90 | mutex.isLocked(); 91 | // true 92 | }); 93 | 94 | // Lock is released. 95 | mutex.isLocked(); 96 | // false 97 | })() 98 | 99 | ``` 100 | 101 | ### Configuration 102 | 103 | `MutexConfigurationInputType` (at the mutex-level) and `LockConfigurationInputType` (at the individual lock-level) can be used to configure the scope and timeouts. 104 | 105 | ```js 106 | /** 107 | * @property holdTimeout The maximum amount of time lock can be held (default: 30000). 108 | * @property key Used to scope mutex (default: a random generated value). 109 | * @property waitTimeout The maximum amount of time lock can be waited for (default: 5000). 110 | */ 111 | type MutexConfigurationInputType = {| 112 | +holdTimeout?: number, 113 | +key?: string, 114 | +waitTimeout?: number, 115 | |}; 116 | 117 | type LockConfigurationInputType = {| 118 | +holdTimeout?: number, 119 | +key?: string, 120 | +waitTimeout?: number, 121 | |}; 122 | 123 | ``` 124 | 125 | ## Usage examples 126 | 127 | ### Image server example 128 | 129 | [Motivation](#motivation) section of the documentation demonstrates a flawed implementation of an image proxy server. That same service can utilise Mutex to solve the illustrated problem: 130 | 131 | ```js 132 | router.use('/images/:uid', async (incomingMessage, serverResponse) => { 133 | const uid = incomingMessage.params.uid; 134 | 135 | const temporaryDownloadPath = path.resolve( 136 | downloadDirectoryPath, 137 | uid, 138 | ); 139 | 140 | await mutex.lock(async () => { 141 | const temporaryFileExists = await fs.pathExists(temporaryDownloadPath); 142 | 143 | if (!temporaryFileExists) { 144 | try { 145 | await pipeline( 146 | // Some external storage engine. 147 | storage.createReadStream('images/' + uid), 148 | fs.createWriteStream(temporaryDownloadPath), 149 | ); 150 | } catch (error) { 151 | await fs.remove(temporaryDownloadPath); 152 | 153 | throw error; 154 | } 155 | } 156 | }, { 157 | // Fail if image cannot be downloaded within 30 seconds. 158 | holdTimeout: 30000, 159 | 160 | // Restrict concurrency only for every unique image request. 161 | key: uid, 162 | 163 | // Fail subsequent requests if they are not attempted within 5 seconds. 164 | // This would happen if there is a high-concurrency or if the original request is taking a long time. 165 | waitTimeout: 5000, 166 | }); 167 | 168 | fs 169 | .createReadStream(temporaryDownloadPath) 170 | .pipe(serverResponse); 171 | }); 172 | 173 | ``` 174 | 175 | ## Related libraries 176 | 177 | * [await-mutex](https://www.npmjs.com/package/await-mutex) – Similar implementation to mutual-exclusion, but without separation by `key` and timeouts. 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "ava": { 8 | "babel": { 9 | "compileAsTests": [ 10 | "test/helpers/**/*" 11 | ] 12 | }, 13 | "files": [ 14 | "test/mutual-exclusion/**/*" 15 | ], 16 | "require": [ 17 | "@babel/register" 18 | ] 19 | }, 20 | "dependencies": { 21 | "core-js": "^3.6.5", 22 | "es6-error": "^4.1.1", 23 | "roarr": "^2.15.3" 24 | }, 25 | "description": "Mutual Exclusion (mutex) object for JavaScript.", 26 | "devDependencies": { 27 | "@ava/babel": "^1.0.1", 28 | "@babel/cli": "^7.10.5", 29 | "@babel/core": "^7.10.5", 30 | "@babel/node": "^7.10.5", 31 | "@babel/plugin-transform-flow-strip-types": "^7.10.4", 32 | "@babel/preset-env": "^7.10.4", 33 | "@babel/register": "^7.10.5", 34 | "ava": "^3.10.1", 35 | "babel-plugin-istanbul": "^6.0.0", 36 | "babel-plugin-transform-export-default-name": "^2.0.4", 37 | "coveralls": "^3.1.0", 38 | "delay": "^4.3.0", 39 | "eslint": "^7.4.0", 40 | "eslint-config-canonical": "^21.0.0", 41 | "flow-bin": "^0.129.0", 42 | "flow-copy-source": "^2.0.9", 43 | "husky": "^4.2.5", 44 | "nyc": "^15.1.0", 45 | "semantic-release": "^17.1.1" 46 | }, 47 | "engines": { 48 | "node": ">=10.0" 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "npm run lint && npm run test && npm run build" 53 | } 54 | }, 55 | "keywords": [ 56 | "mutex", 57 | "mutual", 58 | "exclusion" 59 | ], 60 | "license": "BSD-3-Clause", 61 | "main": "./dist/index.js", 62 | "name": "mutual-exclusion", 63 | "nyc": { 64 | "all": true, 65 | "exclude": [ 66 | "src/bin", 67 | "src/queries/*.js" 68 | ], 69 | "include": [ 70 | "src/**/*.js" 71 | ], 72 | "instrument": false, 73 | "reporter": [ 74 | "html", 75 | "text-summary" 76 | ], 77 | "require": [ 78 | "@babel/register" 79 | ], 80 | "silent": true, 81 | "sourceMap": false 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "https://github.com/gajus/mutual-exclusion" 86 | }, 87 | "scripts": { 88 | "build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps && flow-copy-source src dist", 89 | "dev": "NODE_ENV=development babel ./src --out-dir ./dist --copy-files --source-maps --watch", 90 | "lint": "eslint ./src ./test && flow", 91 | "test": "NODE_ENV=test nyc ava --verbose --serial" 92 | }, 93 | "version": "0.0.0-development" 94 | } 95 | -------------------------------------------------------------------------------- /src/Logger.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Roarr from 'roarr'; 4 | 5 | const Logger = Roarr 6 | .child({ 7 | package: 'mutual-exclusion', 8 | }); 9 | 10 | export default Logger; 11 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable fp/no-class, fp/no-this */ 4 | 5 | import ExtendableError from 'es6-error'; 6 | 7 | export class MutualExclusionError extends ExtendableError {} 8 | 9 | export class WaitTimeoutError extends MutualExclusionError { 10 | code: string; 11 | 12 | constructor (message: string, code: string = 'WAIT_TIMEOUT') { 13 | super(message); 14 | 15 | this.code = code; 16 | } 17 | } 18 | 19 | export class HoldTimeoutError extends MutualExclusionError { 20 | code: string; 21 | 22 | constructor (message: string, code: string = 'HOLD_TIMEOUT') { 23 | super(message); 24 | 25 | this.code = code; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/factories/createMutex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | HoldTimeoutError, 5 | WaitTimeoutError, 6 | } from '../errors'; 7 | import type { 8 | MutexConfigurationInputType, 9 | MutexType, 10 | } from '../types'; 11 | 12 | type StoreType = {| 13 | 14 | // eslint-disable-next-line flowtype/no-weak-types 15 | locking: Promise, 16 | lockCount: number, 17 | |}; 18 | 19 | export default (mutexConfigurationInput?: MutexConfigurationInputType): MutexType => { 20 | const defaultKey = 'mutex-' + String(Math.random()); 21 | 22 | const mutexConfiguration = { 23 | holdTimeout: 30000, 24 | key: defaultKey, 25 | waitTimeout: 5000, 26 | ...mutexConfigurationInput, 27 | }; 28 | 29 | const stores = {}; 30 | 31 | return { 32 | isLocked: (key: string = defaultKey): boolean => { 33 | return Boolean(stores[key]); 34 | }, 35 | lock: async ( 36 | routine, 37 | lockConfigurationInput = mutexConfiguration, 38 | ) => { 39 | const lockConfiguration = { 40 | // $FlowFixMe 41 | ...mutexConfiguration, 42 | ...lockConfigurationInput, 43 | }; 44 | 45 | if (!stores[lockConfiguration.key]) { 46 | stores[lockConfiguration.key] = { 47 | lockCount: 0, 48 | locking: Promise.resolve(), 49 | }; 50 | } 51 | 52 | const store: StoreType = stores[lockConfiguration.key]; 53 | 54 | store.lockCount++; 55 | 56 | let unlockNext; 57 | 58 | // Creates a promise that is resolved by calling `unlockNext`. 59 | // `unlockNext` is result of `willUnlock`. 60 | // On first call, `willUnlock` is resolved immediately. 61 | // However, afterwards `willLock` is added to `locking`, i.e. 62 | // `willLock` locks `locking` until the associated `unlockNext` is called. 63 | const willLock = new Promise((resolve) => { 64 | unlockNext = () => { 65 | store.lockCount--; 66 | 67 | if (store.lockCount === 0) { 68 | // eslint-disable-next-line fp/no-delete 69 | delete stores[lockConfiguration.key]; 70 | } 71 | 72 | resolve(); 73 | }; 74 | }); 75 | 76 | const willUnlock = store.locking 77 | .then(() => { 78 | return Promise.race([ 79 | routine(), 80 | new Promise((resolve, reject) => { 81 | setTimeout(() => { 82 | reject(new HoldTimeoutError('Hold timeout.')); 83 | }, lockConfiguration.holdTimeout); 84 | }), 85 | ]); 86 | }) 87 | .then(() => { 88 | return unlockNext(); 89 | }); 90 | 91 | store.locking = store.locking 92 | .then(() => { 93 | return willLock; 94 | }); 95 | 96 | const waitTimeout = new Promise((resolve, reject) => { 97 | setTimeout(() => { 98 | reject(new WaitTimeoutError('Wait timeout.')); 99 | }, lockConfiguration.waitTimeout); 100 | }); 101 | 102 | // Suppress unhandled rejection error. 103 | willLock.catch(() => {}); 104 | store.locking.catch(() => {}); 105 | 106 | await Promise.race([ 107 | willUnlock, 108 | waitTimeout, 109 | ]); 110 | }, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /src/factories/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export {default as createMutex} from './createMutex'; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export {createMutex} from './factories'; 4 | export { 5 | HoldTimeoutError, 6 | MutualExclusionError, 7 | WaitTimeoutError, 8 | } from './errors'; 9 | export type { 10 | LockConfigurationInputType, 11 | MutexConfigurationInputType, 12 | MutexType, 13 | } from './types'; 14 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * @property holdTimeout The maximum amount of time lock can be held (default: 30000). 5 | * @property key Used to scope mutex (default: a random generated value). 6 | * @property waitTimeout The maximum amount of time lock can be waited for (default: 5000). 7 | */ 8 | export type MutexConfigurationInputType = {| 9 | +holdTimeout?: number, 10 | +key?: string, 11 | +waitTimeout?: number, 12 | |}; 13 | 14 | export type LockConfigurationInputType = {| 15 | +holdTimeout?: number, 16 | +key?: string, 17 | +waitTimeout?: number, 18 | |}; 19 | 20 | export type MutexType = {| 21 | +isLocked: (key?: string) => boolean, 22 | +lock: ( 23 | routine: () => Promise, 24 | lockConfigurationInput?: LockConfigurationInputType, 25 | ) => Promise, 26 | |}; 27 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical/ava", 3 | "rules": { 4 | "filenames/match-regex": 0, 5 | "flowtype/no-flow-fix-me-comments": 0, 6 | "flowtype/no-weak-types": 0, 7 | "id-length": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/mutual-exclusion/factories/createMutex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import test from 'ava'; 4 | import delay from 'delay'; 5 | import createMutex from '../../../src/factories/createMutex'; 6 | import { 7 | HoldTimeoutError, 8 | WaitTimeoutError, 9 | } from '../../../src/errors'; 10 | 11 | test('creates and releases a lock', async (t) => { 12 | const mutex = createMutex(); 13 | 14 | await mutex.lock(async () => { 15 | t.true(mutex.isLocked()); 16 | }); 17 | 18 | await delay(50); 19 | 20 | t.false(mutex.isLocked()); 21 | }); 22 | 23 | test('blocks lock until an existing lock is released', async (t) => { 24 | const mutex = createMutex(); 25 | 26 | mutex.lock(async () => { 27 | await delay(50); 28 | }); 29 | 30 | t.true(mutex.isLocked()); 31 | 32 | let secondLockPending = true; 33 | 34 | // eslint-disable-next-line promise/catch-or-return 35 | mutex.lock(async () => {}) 36 | .then((value) => { 37 | secondLockPending = false; 38 | 39 | return value; 40 | }); 41 | 42 | t.true(secondLockPending); 43 | 44 | await delay(100); 45 | 46 | t.false(secondLockPending); 47 | 48 | t.false(mutex.isLocked()); 49 | }); 50 | 51 | test('resolves locks in FIFO order', async (t) => { 52 | const mutex = createMutex(); 53 | 54 | mutex.lock(async () => { 55 | await delay(50); 56 | }); 57 | 58 | let secondLockPending = true; 59 | 60 | // eslint-disable-next-line promise/catch-or-return 61 | mutex.lock(async () => { 62 | await delay(50); 63 | }) 64 | .then((value) => { 65 | secondLockPending = false; 66 | 67 | return value; 68 | }); 69 | 70 | let thirdLockPending = true; 71 | 72 | // eslint-disable-next-line promise/catch-or-return 73 | mutex.lock(async () => { 74 | await delay(50); 75 | }) 76 | .then((value) => { 77 | thirdLockPending = false; 78 | 79 | return value; 80 | }); 81 | 82 | t.true(mutex.isLocked()); 83 | t.true(secondLockPending); 84 | t.true(thirdLockPending); 85 | 86 | await delay(150); 87 | 88 | t.true(mutex.isLocked()); 89 | t.false(secondLockPending); 90 | t.true(thirdLockPending); 91 | 92 | await delay(200); 93 | 94 | t.false(mutex.isLocked()); 95 | t.false(secondLockPending); 96 | t.false(thirdLockPending); 97 | }); 98 | 99 | test('rejects lock after the hold timeout is reached (mutex configuration)', async (t) => { 100 | const mutex = createMutex({ 101 | holdTimeout: 50, 102 | }); 103 | 104 | await t.throwsAsync(mutex.lock(async () => { 105 | await delay(100); 106 | }), { 107 | code: 'HOLD_TIMEOUT', 108 | instanceOf: HoldTimeoutError, 109 | message: 'Hold timeout.', 110 | }); 111 | }); 112 | 113 | test('rejects lock after the hold timeout is reached (lock configuration)', async (t) => { 114 | const mutex = createMutex({ 115 | holdTimeout: 500, 116 | }); 117 | 118 | await t.throwsAsync(mutex.lock(async () => { 119 | await delay(100); 120 | }, { 121 | holdTimeout: 50, 122 | }), { 123 | code: 'HOLD_TIMEOUT', 124 | instanceOf: HoldTimeoutError, 125 | message: 'Hold timeout.', 126 | }); 127 | }); 128 | 129 | test('rejects lock after the wait timeout is reached (mutex configuration)', async (t) => { 130 | const mutex = createMutex({ 131 | waitTimeout: 50, 132 | }); 133 | 134 | mutex.lock(async () => { 135 | await delay(100); 136 | }, { 137 | waitTimeout: 500, 138 | }); 139 | 140 | await t.throwsAsync(mutex.lock(async () => {}), { 141 | code: 'WAIT_TIMEOUT', 142 | instanceOf: WaitTimeoutError, 143 | message: 'Wait timeout.', 144 | }); 145 | }); 146 | 147 | test('rejects lock after the wait timeout is reached (lock configuration)', async (t) => { 148 | const mutex = createMutex({ 149 | waitTimeout: 500, 150 | }); 151 | 152 | mutex.lock(async () => { 153 | await delay(100); 154 | }); 155 | 156 | await t.throwsAsync(mutex.lock(async () => {}, { 157 | waitTimeout: 50, 158 | }), { 159 | code: 'WAIT_TIMEOUT', 160 | instanceOf: WaitTimeoutError, 161 | message: 'Wait timeout.', 162 | }); 163 | }); 164 | --------------------------------------------------------------------------------