[
  {
    "path": ".codeclimate.yml",
    "content": "version: '2'\nplugins:\n  eslint:\n    enabled: true\n    channel: 'eslint-6'\n"
  },
  {
    "path": ".fossa.yml",
    "content": "# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)\n# Visit https://fossa.com to learn more\n\nversion: 2\ncli:\n  server: https://app.fossa.com\n  fetcher: custom\n  project: git@github.com:swarthy/redis-semaphore.git\nanalyze:\n  modules:\n  - name: .\n    type: npm\n    target: .\n    path: .\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'monthly'\n"
  },
  {
    "path": ".github/workflows/branches.yml",
    "content": "name: CI (push)\n\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch:\n\njobs:\n  integration-test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, 22.x, 24.x]\n\n    env:\n      COVERALLS_REPO_TOKEN: '${{ secrets.COVERALLS_REPO_TOKEN }}'\n      COVERALLS_GIT_BRANCH: '${{ github.ref }}'\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Enable Corepack\n        run: corepack enable\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'yarn'\n\n      - run: yarn install --immutable\n\n      - run: docker compose up -d redis1 redis2 redis3\n      - run: docker compose run waiter\n\n      - run: yarn build\n      - run: yarn lint\n      - run: yarn test-ci-with-coverage\n"
  },
  {
    "path": ".github/workflows/pull-requests.yml",
    "content": "name: CI (PR)\n\non:\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n\njobs:\n  integration-test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, 22.x, 24.x]\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Enable Corepack\n        run: corepack enable\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'yarn'\n\n      - run: yarn install --immutable\n\n      - run: docker compose up -d redis1 redis2 redis3\n      - run: docker compose run waiter\n\n      - run: yarn build\n      - run: yarn lint\n      - run: yarn test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# Yarn\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/sdks\n!.yarn/versions\n\n# dotenv environment variables file\n.env\n\nes\nlib\n/.idea/\n"
  },
  {
    "path": ".mocharc.yaml",
    "content": "extension: ts\nrecursive: true\ntimeout: 5s\nrequire:\n  - '@swc-node/register'\n  - test/setup.ts\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\",\n  \"arrowParens\": \"avoid\"\n}\n"
  },
  {
    "path": ".snyk",
    "content": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.13.1\nignore: {}\npatch: {}\n"
  },
  {
    "path": ".yarnrc.yml",
    "content": "nodeLinker: node-modules\ndefaultSemverRangePrefix: ''\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "### redis-semaphore@5.7.0\n\n- Added `AbortSignal` to acquire\n\n### redis-semaphore@5.6.2\n\n- Fixed implicit import from `src`\n- Removed `src` folder from NPM package\n\n### redis-semaphore@5.6.1\n\n- Removed `module` field from `package.json`\n\n### redis-semaphore@5.6.0\n\n- Added interface compatible client support (ex. `ioredis-mock`)\n- Removed `instanceof Redis` validation in constructor\n- `ioredis` marked as optional peerDependency, explicit `ioredis` install is required now\n\n### redis-semaphore@5.5.1\n\n- Fix race condition for refresh started before release and finished after release\n\n### redis-semaphore@5.5.0\n\n- Added `identifier` constructor option.\n- Added `acquiredExternally` constructor option.\n- Option `externallyAcquiredIdentifier` **DEPRECATED**.\n- Option `identifierSuffix` **DEPRECATED**.\n\n### redis-semaphore@5.4.0\n\n- Added `identifierSuffix` option, usefull for tracing app instance which locked resource\n\n### redis-semaphore@5.3.1\n\n- Fixed reacquire expired resource in refresh\n\n### redis-semaphore@5.3.0\n\n- Added `stopRefresh` method\n- Added `externallyAcquiredIdentifier` optional constructor option\n- Removed `uuid` dependency\n\n### redis-semaphore@5.2.0\n\n- Added `acquireAttemptsLimit` method\n\n### redis-semaphore@5.1.0\n\n- Added `tryAcquire`\n\n### redis-semaphore@5.0.0\n\n- **Breadking change:** Drop Node.js v10.x, v12.x support\n- Added `ioredis@5` support\n\n### redis-semaphore@4.1.0\n\n- Added `.isAcquired` property on all locks\n- Added `onLostLock` constructor option. By default throws unhandled error.\n\n### redis-semaphore@4.0.0\n\n- **Breaking change:** `Mutex`, `Semaphore`, `MultiSemaphore` not longer support `Cluster`. For multi-node case use `Redlock*` instead.\n- Added `RedlockMutex`, `RedlockSemaphore`, `RedlockMultiSemaphore`\n- Internals refactored\n\n### redis-semaphore@3.2.0\n\n- Added `MultiSemaphore`\n\n### redis-semaphore@3.0.0\n\n- **Breaking change:** `FairSemaphore` has been removed. Use `Semaphore` instead (has the same \"fairness\")\n  - the `acquire` method in `Semaphore` no longer returns a boolean. Instead, it throws an error if it cannot acquire, and if it doesn't throw, you can assume it worked.\n- Internal code has been cleaned up\n- Added more test, include synthetic node unsynchroned clocks\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine\nRUN npm i -g @swarthy/wait-for@2.0.2\nVOLUME /app\nWORKDIR /app\nUSER node\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Alexander Mochalin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# redis-semaphore\n\n[![NPM version][npm-image]][npm-url]\n[![Build status][ci-image]][ci-url]\n![FOSSA Status][typescript-image]\n[![Coverage Status][coverage-image]][coverage-url]\n[![Maintainability][codeclimate-image]][codeclimate-url]\n[![Known Vulnerabilities][snyk-image]][snyk-url]\n[![FOSSA Status][fossa-badge-image]][fossa-badge-url]\n\n[Mutex](<https://en.wikipedia.org/wiki/Lock_(computer_science)>) and [Semaphore](<https://en.wikipedia.org/wiki/Semaphore_(programming)>) implementations based on [Redis](https://redis.io/) ready for distributed systems\n\n## Features\n\n- Fail-safe (all actions performed by LUA scripts (atomic))\n\n## Usage\n\n### Installation\n\n```bash\nnpm install --save redis-semaphore ioredis\n# or\nyarn add redis-semaphore ioredis\n```\n\nioredis is the officially supported Redis client. This library's test code runs on it.\n\nUsers of other Redis clients should ensure ioredis-compatible API (see src/types.ts) when creating lock objects.\n\n### Mutex\n\n> See [RedisLabs: Locks with timeouts](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-2-distributed-locking/6-2-5-locks-with-timeouts/)\n\n##### new Mutex(redisClient, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8, identifier = crypto.randomUUID() }])\n\n- `redisClient` - **required**, configured `redis` client\n- `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)\n- `options` - _optional_\n  - `lockTimeout` - _optional_ ms, time after mutex will be auto released (expired)\n  - `acquireTimeout` - _optional_ ms, max timeout for `.acquire()` call\n  - `acquireAttemptsLimit` - _optional_ max number of attempts to be made in `.acquire()` call\n  - `retryInterval` - _optional_ ms, time between acquire attempts if resource locked\n  - `refreshInterval` - _optional_ ms, auto-refresh interval; to disable auto-refresh behaviour set `0`\n  - `identifier` - _optional_ uuid, custom mutex identifier. Must be unique between parallel executors, otherwise multiple locks with same identifier *can* be treated as the same lock holder. Override only if you know what you are doing (see `acquiredExternally` option).\n  - `acquiredExternally` - _optional_ `true`, If `identifier` provided and `acquiredExternally` is `true` then `_refresh` will be used instead of `_acquire` in `.tryAcquire()`/`.acquire()`. Useful for lock sharing between processes: acquire in scheduler, refresh and release in handler.\n  - `onLockLost` - _optional_ function, called when lock loss is detected due refresh cycle; default onLockLost throws unhandled LostLockError\n\n#### Example\n\n```javascript\nconst Mutex = require('redis-semaphore').Mutex\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { Mutex } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClient = new Redis()\n\nasync function doSomething() {\n  const mutex = new Mutex(redisClient, 'lockingResource')\n  await mutex.acquire()\n  try {\n    // critical code\n  } finally {\n    await mutex.release()\n  }\n}\n```\n\n#### Example with lost lock handling\n\n```javascript\nasync function doSomething() {\n  const mutex = new Mutex(redisClient, 'lockingResource', {\n    // By default onLockLost throws unhandled LostLockError\n    onLockLost(err) {\n      console.error(err)\n    }\n  })\n  await mutex.acquire()\n  try {\n    while (mutex.isAcquired) {\n      // critical cycle iteration\n    }\n  } finally {\n    // It's safe to always call release, because if lock is no longer belongs to this mutex, .release() will have no effect\n    await mutex.release()\n  }\n}\n```\n\n#### Example with optional lock\n\n```javascript\nasync function doSomething() {\n  const mutex = new Mutex(redisClient, 'lockingResource', {\n    acquireAttemptsLimit: 1\n  })\n  const lockAcquired = await mutex.tryAcquire()\n  if (!lockAcquired) {\n    return\n  }\n  try {\n    while (mutex.isAcquired) {\n      // critical cycle iteration\n    }\n  } finally {\n    // It's safe to always call release, because if lock is no longer belongs to this mutex, .release() will have no effect\n    await mutex.release()\n  }\n}\n```\n\n#### Example with temporary refresh\n\n```javascript\nasync function doSomething() {\n  const mutex = new Mutex(redisClient, 'lockingResource', {\n    lockTimeout: 120000,\n    refreshInterval: 15000\n  })\n  const lockAcquired = await mutex.tryAcquire()\n  if (!lockAcquired) {\n    return\n  }\n  try {\n    // critical cycle iteration\n  } finally {\n    // We want to let lock expire over time after operation is finished\n    await mutex.stopRefresh()\n  }\n}\n```\n\n#### Example with dynamically adjusting existing lock\n\n```javascript\nconst Mutex = require('redis-semaphore').Mutex\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { Mutex } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClient = new Redis()\n\n// This creates an original lock\nconst preMutex = new Mutex(redisClient, 'lockingResource', {\n  lockTimeout: 10 * 1e3, // lock for 10s\n  refreshInterval: 0\n});\n\n// This modifies lock with a new TTL and starts refresh\nconst mutex = new Mutex(redisClient, 'lockingResource', {\n  identifier: preMutex.identifier,\n  acquiredExternally: true, // required in this case\n  lockTimeout: 30 * 60 * 1e3, // lock for 30min\n  refreshInterval: 60 * 1e3\n});\n\n```\n\n#### Example with shared lock between scheduler and handler apps\n\n```javascript\nconst Mutex = require('redis-semaphore').Mutex\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { Mutex } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClient = new Redis()\n\n// scheduler app code\nasync function every10MinutesCronScheduler() {\n  const mutex = new Mutex(redisClient, 'lockingResource', {\n    lockTimeout: 30 * 60 * 1e3, // lock for 30min\n    refreshInterval: 0\n  })\n  if (await mutex.tryAcquire()) {\n    someQueue.publish({ mutexIdentifier: mutex.identifier })\n  } else {\n    logger.info('Job already scheduled. Do nothing in current cron cycle')\n  }\n}\n\n// handler app code\nasync function queueHandler(queueMessageData) {\n  const { mutexIdentifier } = queueMessageData\n  const mutex = new Mutex(redisClient, 'lockingResource', {\n    lockTimeout: 10 * 1e3, // 10sec\n    identifier: mutexIdentifier,\n    acquiredExternally: true // required in this case\n  })\n\n  // actually will do `refresh` with new lockTimeout instead of acquire\n  // if mutex was locked by another process or lock was expired - exception will be thrown (default refresh behavior)\n  await mutex.acquire()\n\n  try {\n    // critical code\n  } finally {\n    await mutex.release()\n  }\n}\n```\n\n### Semaphore\n\n> See [RedisLabs: Basic counting sempahore](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/)\n\nThis implementation is slightly different from the algorithm described in the book, but the main idea has not changed.\n\n`zrank` check replaced with `zcard`, so now it is fair as [RedisLabs: Fair semaphore](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-2-fair-semaphores/) (see tests).\n\nIn edge cases (node time difference is greater than `lockTimeout`) both algorithms are not fair due cleanup stage (removing expired members from sorted set), so `FairSemaphore` API has been removed (it's safe to replace it with `Semaphore`).\n\nMost reliable way to use: `lockTimeout` is greater than possible node clock differences, `refreshInterval` is not 0 and is less enough than `lockTimeout` (by default is `lockTimeout * 0.8`)\n\n##### new Semaphore(redisClient, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])\n\n- `redisClient` - **required**, configured `redis` client\n- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)\n- `maxCount` - **required**, maximum simultaneously resource usage count\n- `options` _optional_ See `Mutex` options\n\n#### Example\n\n```javascript\nconst Semaphore = require('redis-semaphore').Semaphore\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { Semaphore } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClient = new Redis()\n\nasync function doSomething() {\n  const semaphore = new Semaphore(redisClient, 'lockingResource', 5)\n  await semaphore.acquire()\n  try {\n    // maximum 5 simultaneously executions\n  } finally {\n    await semaphore.release()\n  }\n}\n```\n\n### MultiSemaphore\n\nSame as `Semaphore` with one difference - MultiSemaphore will try to acquire multiple permits instead of one.\n\n`MultiSemaphore` and `Semaphore` shares same key namespace and can be used together (see test/src/RedisMultiSemaphore.test.ts).\n\n##### new MultiSemaphore(redisClient, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])\n\n- `redisClient` - **required**, configured `redis` client\n- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)\n- `maxCount` - **required**, maximum simultaneously resource usage count\n- `permits` - **required**, number of acquiring permits\n- `options` _optional_ See `Mutex` options\n\n#### Example\n\n```javascript\nconst MultiSemaphore = require('redis-semaphore').MultiSemaphore\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { MultiSemaphore } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClient = new Redis()\n\nasync function doSomething() {\n  const semaphore = new MultiSemaphore(redisClient, 'lockingResource', 5, 2)\n\n  await semaphore.acquire()\n  try {\n    // make 2 parallel calls to remote service which allow only 5 simultaneously calls\n  } finally {\n    await semaphore.release()\n  }\n}\n```\n\n### RedlockMutex\n\nDistributed `Mutex` version\n\n> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)\n\n##### new RedlockMutex(redisClients, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])\n\n- `redisClients` - **required**, array of configured `redis` client connected to independent nodes\n- `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)\n- `options` _optional_ See `Mutex` options\n\n#### Example\n\n```javascript\nconst RedlockMutex = require('redis-semaphore').RedlockMutex\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { RedlockMutex } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClients = [\n  new Redis('127.0.0.1:6377'),\n  new Redis('127.0.0.1:6378'),\n  new Redis('127.0.0.1:6379')\n] // \"Those nodes are totally independent, so we don’t use replication or any other implicit coordination system.\"\n\nasync function doSomething() {\n  const mutex = new RedlockMutex(redisClients, 'lockingResource')\n  await mutex.acquire()\n  try {\n    // critical code\n  } finally {\n    await mutex.release()\n  }\n}\n```\n\n### RedlockSemaphore\n\nDistributed `Semaphore` version\n\n> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)\n\n##### new RedlockSemaphore(redisClients, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])\n\n- `redisClients` - **required**, array of configured `redis` client connected to independent nodes\n- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)\n- `maxCount` - **required**, maximum simultaneously resource usage count\n- `options` _optional_ See `Mutex` options\n\n#### Example\n\n```javascript\nconst RedlockSemaphore = require('redis-semaphore').RedlockSemaphore\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { RedlockSemaphore } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClients = [\n  new Redis('127.0.0.1:6377'),\n  new Redis('127.0.0.1:6378'),\n  new Redis('127.0.0.1:6379')\n] // \"Those nodes are totally independent, so we don’t use replication or any other implicit coordination system.\"\n\nasync function doSomething() {\n  const semaphore = new Semaphore(redisClients, 'lockingResource', 5)\n  await semaphore.acquire()\n  try {\n    // maximum 5 simultaneously executions\n  } finally {\n    await semaphore.release()\n  }\n}\n```\n\n### RedlockMultiSemaphore\n\nDistributed `MultiSemaphore` version\n\n> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)\n\n##### new RedlockMultiSemaphore(redisClients, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])\n\n- `redisClients` - **required**, array of configured `redis` client connected to independent nodes\n- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)\n- `maxCount` - **required**, maximum simultaneously resource usage count\n- `permits` - **required**, number of acquiring permits\n- `options` _optional_ See `Mutex` options\n\n#### Example\n\n```javascript\nconst RedlockMultiSemaphore = require('redis-semaphore').RedlockMultiSemaphore\nconst Redis = require('ioredis')\n\n// TypeScript\n// import { RedlockMultiSemaphore } from 'redis-semaphore'\n// import Redis from 'ioredis'\n\nconst redisClients = [\n  new Redis('127.0.0.1:6377'),\n  new Redis('127.0.0.1:6378'),\n  new Redis('127.0.0.1:6379')\n] // \"Those nodes are totally independent, so we don’t use replication or any other implicit coordination system.\"\n\nasync function doSomething() {\n  const semaphore = new RedlockMultiSemaphore(\n    redisClients,\n    'lockingResource',\n    5,\n    2\n  )\n\n  await semaphore.acquire()\n  try {\n    // make 2 parallel calls to remote service which allow only 5 simultaneously calls\n  } finally {\n    await semaphore.release()\n  }\n}\n```\n\n## Development\n\n```shell\nyarn --immutable\n./setup-redis-servers.sh\nyarn dev\n```\n\n## License\n\nMIT\n\n[![FOSSA Status][fossa-large-image]][fossa-large-url]\n\n[npm-image]: https://img.shields.io/npm/v/redis-semaphore.svg?style=flat-square\n[npm-url]: https://npmjs.org/package/redis-semaphore\n[ci-image]: https://github.com/swarthy/redis-semaphore/actions/workflows/branches.yml/badge.svg\n[ci-url]: https://github.com/swarthy/redis-semaphore/actions/workflows/branches.yml\n[codeclimate-image]: https://api.codeclimate.com/v1/badges/02778c96bb5983eb150c/maintainability\n[codeclimate-url]: https://codeclimate.com/github/swarthy/redis-semaphore/maintainability\n[snyk-image]: https://snyk.io/test/npm/redis-semaphore/badge.svg\n[snyk-url]: https://snyk.io/test/npm/redis-semaphore\n[coverage-image]: https://coveralls.io/repos/github/swarthy/redis-semaphore/badge.svg?branch=master\n[coverage-url]: https://coveralls.io/r/swarthy/redis-semaphore?branch=master\n[fossa-badge-image]: https://app.fossa.com/api/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git.svg?type=shield\n[fossa-badge-url]: https://app.fossa.com/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git?ref=badge_shield\n[fossa-large-image]: https://app.fossa.com/api/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git.svg?type=large\n[fossa-large-url]: https://app.fossa.com/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git?ref=badge_large\n[typescript-image]: https://badgen.net/npm/types/tslib\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.7'\n\nservices:\n  waiter:\n    image: node:24-alpine\n    volumes:\n      - ./:/app\n    working_dir: /app\n    command: >\n      sh -c \"\n        corepack --version && corepack enable &&\n        echo 'Wait for all redis servers...' &&\n        yarn wait-for --redis redis://redis1 &&\n        yarn wait-for --redis redis://redis2 &&\n        yarn wait-for --redis redis://redis3 &&\n        echo 'All redis instances ready!'\n      \"\n\n  redis1:\n    image: redis:alpine\n    ports:\n      - 6001:6379\n\n  redis2:\n    image: redis:alpine\n    ports:\n      - 6002:6379\n\n  redis3:\n    image: redis:alpine\n    ports:\n      - 6003:6379\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import typescript from '@typescript-eslint/eslint-plugin'\nimport typescriptParser from '@typescript-eslint/parser'\nimport nodePlugin from 'eslint-plugin-node'\n\nexport default [\n  {\n    ignores: ['lib/**', 'es/**', 'coverage/**', '.nyc_output/**']\n  },\n  {\n    files: ['**/*.ts'],\n    languageOptions: {\n      parser: typescriptParser,\n      parserOptions: {\n        ecmaVersion: 2024,\n        sourceType: 'module',\n        project: './tsconfig.json',\n        ecmaFeatures: {\n          jsx: false\n        }\n      },\n      globals: {\n        console: true,\n        process: true,\n        setTimeout: true,\n        clearTimeout: true,\n        setInterval: true,\n        clearInterval: true\n      }\n    },\n    plugins: {\n      '@typescript-eslint': typescript\n    },\n    rules: {\n      ...typescript.configs['recommended'].rules,\n      ...typescript.configs['recommended-requiring-type-checking'].rules,\n      '@typescript-eslint/explicit-function-return-type': 'error',\n      '@typescript-eslint/no-explicit-any': 'error',\n      '@typescript-eslint/no-unused-vars': [\n        'error',\n        { argsIgnorePattern: '^_' }\n      ],\n      '@typescript-eslint/no-unused-expressions': 'off',\n      '@typescript-eslint/no-misused-promises': 'off',\n      '@typescript-eslint/require-await': 'off',\n      'no-unused-expressions': 'off'\n    }\n  },\n  {\n    files: ['test/**/*.ts'],\n    languageOptions: {\n      globals: {\n        describe: true,\n        it: true,\n        before: true,\n        after: true,\n        beforeEach: true,\n        afterEach: true,\n        mocha: true\n      }\n    },\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-unsafe-assignment': 'off',\n      '@typescript-eslint/no-unsafe-member-access': 'off',\n      '@typescript-eslint/no-unsafe-call': 'off',\n      '@typescript-eslint/no-unsafe-return': 'off',\n      '@typescript-eslint/no-unsafe-argument': 'off',\n      '@typescript-eslint/await-thenable': 'off',\n      '@typescript-eslint/explicit-function-return-type': 'off',\n      'no-unused-vars': 'off'\n    }\n  }\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"redis-semaphore\",\n  \"version\": \"5.7.0\",\n  \"description\": \"Distributed mutex and semaphore based on Redis\",\n  \"main\": \"lib/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint --ext .js,.ts .\",\n    \"test\": \"mocha\",\n    \"test-ci-with-coverage\": \"nyc mocha && nyc report --reporter=text-lcov | coveralls\",\n    \"coverage-html\": \"nyc mocha && nyc report --reporter=html\",\n    \"converalls\": \"nyc mocha && nyc report --reporter=text-lcov | coveralls\",\n    \"dev\": \"mocha -w\",\n    \"build\": \"yarn build-commonjs\",\n    \"build-commonjs\": \"rm -rf lib && yarn tsc -b tsconfig.build-commonjs.json\",\n    \"build-es\": \"rm -rf es && yarn tsc -b tsconfig.build-es.json\",\n    \"preversion\": \"yarn lint && yarn test && yarn build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:swarthy/redis-semaphore.git\"\n  },\n  \"keywords\": [\n    \"redis\",\n    \"redlock\",\n    \"mutex\",\n    \"semaphore\"\n  ],\n  \"author\": \"Alexander Mochalin (horroshow@mail.ru)\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@swarthy/wait-for\": \"^2.1.1\",\n    \"@swc-node/register\": \"1.10.10\",\n    \"@swc/core\": \"1.11.11\",\n    \"@types/chai\": \"^4.3.20\",\n    \"@types/chai-as-promised\": \"^7.1.8\",\n    \"@types/debug\": \"^4.1.12\",\n    \"@types/ioredis-mock\": \"^8.2.5\",\n    \"@types/mocha\": \"^10.0.10\",\n    \"@types/node\": \"22.13.11\",\n    \"@types/sinon\": \"^17.0.4\",\n    \"@types/sinon-chai\": \"^3.2.12\",\n    \"@typescript-eslint/eslint-plugin\": \"8.27.0\",\n    \"@typescript-eslint/parser\": \"8.27.0\",\n    \"benchmark\": \"^2.1.4\",\n    \"chai\": \"4.5.0\",\n    \"chai-as-promised\": \"7.1.2\",\n    \"coveralls\": \"^3.1.1\",\n    \"eslint\": \"9.23.0\",\n    \"eslint-plugin-node\": \"11.1.0\",\n    \"ioredis\": \"5.6.0\",\n    \"ioredis-mock\": \"8.9.0\",\n    \"mocha\": \"11.1.0\",\n    \"mocha-lcov-reporter\": \"^1.3.0\",\n    \"nyc\": \"^17.1.0\",\n    \"sinon\": \"19.0.4\",\n    \"sinon-chai\": \"3.7.0\",\n    \"snyk\": \"1.1296.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">= 14.17.0\"\n  },\n  \"peerDependencies\": {\n    \"ioredis\": \"^4.1.0 || ^5\"\n  },\n  \"peerDependenciesMeta\": {\n    \"ioredis\": {\n      \"optional\": true\n    }\n  },\n  \"dependencies\": {\n    \"debug\": \"^4.4.0\"\n  },\n  \"packageManager\": \"yarn@4.1.0+sha256.81a00df816059803e6b5148acf03ce313cad36b7f6e5af6efa040a15981a6ffb\",\n  \"files\": [\n    \"lib/\"\n  ]\n}\n"
  },
  {
    "path": "setup-redis-servers.sh",
    "content": "#!/bin/sh\ndocker compose up -d redis1 redis2 redis3\n"
  },
  {
    "path": "src/Lock.ts",
    "content": "import createDebug from 'debug'\nimport * as crypto from 'node:crypto'\nimport LostLockError from './errors/LostLockError'\nimport TimeoutError from './errors/TimeoutError'\nimport { defaultOnLockLost, defaultTimeoutOptions } from './misc'\nimport { AcquireOptions, LockLostCallback, LockOptions } from './types'\n\nconst REFRESH_INTERVAL_COEF = 0.8\n\nconst debug = createDebug('redis-semaphore:instance')\n\nexport abstract class Lock {\n  protected abstract _kind: string\n  protected abstract _key: string\n  protected _identifier: string\n  protected _acquireOptions: AcquireOptions\n  protected _refreshTimeInterval: number\n  protected _refreshInterval?: ReturnType<typeof setInterval>\n  protected _refreshing = false\n  protected _acquired = false\n  protected _acquiredExternally = false\n  protected _onLockLost: LockLostCallback\n\n  protected abstract _refresh(): Promise<boolean>\n  protected abstract _acquire(abortSignal?: AbortSignal): Promise<boolean>\n  protected abstract _release(): Promise<void>\n\n  constructor({\n    lockTimeout = defaultTimeoutOptions.lockTimeout,\n    acquireTimeout = defaultTimeoutOptions.acquireTimeout,\n    acquireAttemptsLimit = defaultTimeoutOptions.acquireAttemptsLimit,\n    retryInterval = defaultTimeoutOptions.retryInterval,\n    refreshInterval = Math.round(lockTimeout * REFRESH_INTERVAL_COEF),\n    onLockLost = defaultOnLockLost,\n    externallyAcquiredIdentifier,\n    identifierSuffix,\n    identifier,\n    acquiredExternally\n  }: LockOptions = defaultTimeoutOptions) {\n    if (\n      identifier !== undefined &&\n      (!identifier || typeof identifier !== 'string')\n    ) {\n      throw new Error('identifier must be not empty random string')\n    }\n    if (acquiredExternally && !identifier) {\n      throw new Error(\n        'acquiredExternally=true meanless without custom identifier'\n      )\n    }\n    if (externallyAcquiredIdentifier && (identifier || acquiredExternally)) {\n      throw new Error(\n        'Invalid usage. Use custom identifier and acquiredExternally: true'\n      )\n    }\n    this._identifier =\n      identifier ||\n      externallyAcquiredIdentifier ||\n      this.getIdentifier(identifierSuffix)\n    this._acquiredExternally =\n      !!acquiredExternally || !!externallyAcquiredIdentifier\n    this._acquireOptions = {\n      lockTimeout,\n      acquireTimeout,\n      acquireAttemptsLimit,\n      retryInterval,\n      identifier: this._identifier\n    }\n    this._refreshTimeInterval = refreshInterval\n    this._onLockLost = onLockLost\n  }\n\n  get identifier(): string {\n    return this._identifier\n  }\n\n  get isAcquired(): boolean {\n    return this._acquired\n  }\n\n  private getIdentifier(identifierSuffix: string | undefined): string {\n    const uuid = crypto.randomUUID()\n    return identifierSuffix ? `${uuid}-${identifierSuffix}` : uuid\n  }\n\n  private _startRefresh(): void {\n    this._refreshInterval = setInterval(\n      this._processRefresh,\n      this._refreshTimeInterval\n    )\n    this._refreshInterval.unref()\n  }\n\n  stopRefresh(): void {\n    if (this._refreshInterval) {\n      debug(\n        `clear refresh interval ${this._kind} (key: ${this._key}, identifier: ${this._identifier})`\n      )\n      clearInterval(this._refreshInterval)\n    }\n  }\n\n  private _processRefresh = async (): Promise<void> => {\n    if (this._refreshing) {\n      debug(\n        `already refreshing ${this._kind} (key: ${this._key}, identifier: ${this._identifier}) (skip)`\n      )\n      return\n    }\n    this._refreshing = true\n    try {\n      debug(\n        `refresh ${this._kind} (key: ${this._key}, identifier: ${this._identifier})`\n      )\n      const refreshed = await this._refresh()\n      if (!refreshed) {\n        if (!this._acquired) {\n          debug(\n            `refresh ${this._kind} (key: ${this._key}, identifier: ${this._identifier} failed, but lock was purposefully released`\n          )\n          return\n        }\n        this._acquired = false\n        this.stopRefresh()\n        const lockLostError = new LostLockError(\n          `Lost ${this._kind} for key ${this._key}`\n        )\n        this._onLockLost(lockLostError)\n      }\n    } finally {\n      this._refreshing = false\n    }\n  }\n\n  async acquire(abortSignal?: AbortSignal): Promise<void> {\n    debug(`acquire ${this._kind} (key: ${this._key})`)\n    const acquired = await this.tryAcquire(abortSignal)\n    if (!acquired) {\n      throw new TimeoutError(`Acquire ${this._kind} ${this._key} timeout`)\n    }\n  }\n\n  async tryAcquire(abortSignal?: AbortSignal): Promise<boolean> {\n    debug(`tryAcquire ${this._kind} (key: ${this._key})`)\n    const acquired = this._acquiredExternally\n      ? await this._refresh()\n      : await this._acquire(abortSignal)\n    if (!acquired) {\n      return false\n    }\n    this._acquired = true\n    this._acquiredExternally = false\n    if (this._refreshTimeInterval > 0) {\n      this._startRefresh()\n    }\n    return true\n  }\n\n  async release(): Promise<void> {\n    debug(\n      `release ${this._kind} (key: ${this._key}, identifier: ${this._identifier})`\n    )\n    if (this._refreshTimeInterval > 0) {\n      this.stopRefresh()\n    }\n    if (this._acquired || this._acquiredExternally) {\n      await this._release()\n    }\n    this._acquired = false\n    this._acquiredExternally = false\n  }\n}\n"
  },
  {
    "path": "src/RedisMultiSemaphore.ts",
    "content": "import { acquireSemaphore } from './multiSemaphore/acquire'\nimport { refreshSemaphore } from './multiSemaphore/refresh'\nimport { releaseSemaphore } from './multiSemaphore/release'\nimport RedisSemaphore from './RedisSemaphore'\nimport { LockOptions, RedisClient } from './types'\n\nexport default class RedisMultiSemaphore extends RedisSemaphore {\n  protected _kind = 'multi-semaphore'\n  protected _permits: number\n\n  constructor(\n    client: RedisClient,\n    key: string,\n    limit: number,\n    permits: number,\n    options?: LockOptions\n  ) {\n    super(client, key, limit, options)\n    if (!permits) {\n      throw new Error('\"permits\" is required')\n    }\n    if (typeof permits !== 'number') {\n      throw new Error('\"permits\" must be a number')\n    }\n    this._permits = permits\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshSemaphore(\n      this._client,\n      this._key,\n      this._limit,\n      this._permits,\n      this._acquireOptions\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireSemaphore(\n      this._client,\n      this._key,\n      this._limit,\n      this._permits,\n      this._acquireOptions,\n      abortSignal\n    )\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseSemaphore(\n      this._client,\n      this._key,\n      this._permits,\n      this._identifier\n    )\n  }\n}\n"
  },
  {
    "path": "src/RedisMutex.ts",
    "content": "import type { RedisClient } from './types'\n\nimport { Lock } from './Lock'\nimport { acquireMutex } from './mutex/acquire'\nimport { refreshMutex } from './mutex/refresh'\nimport { releaseMutex } from './mutex/release'\nimport { LockOptions } from './types'\n\nexport default class RedisMutex extends Lock {\n  protected _kind = 'mutex'\n  protected _key: string\n  protected _client: RedisClient\n\n  constructor(client: RedisClient, key: string, options?: LockOptions) {\n    super(options)\n    if (!client) {\n      throw new Error('\"client\" is required')\n    }\n    if (!key) {\n      throw new Error('\"key\" is required')\n    }\n    if (typeof key !== 'string') {\n      throw new Error('\"key\" must be a string')\n    }\n    this._client = client\n    this._key = `mutex:${key}`\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshMutex(\n      this._client,\n      this._key,\n      this._identifier,\n      this._acquireOptions.lockTimeout\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireMutex(this._client, this._key, this._acquireOptions, abortSignal)\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseMutex(this._client, this._key, this._identifier)\n  }\n}\n"
  },
  {
    "path": "src/RedisSemaphore.ts",
    "content": "import RedisMutex from './RedisMutex'\nimport { acquireSemaphore } from './semaphore/acquire'\nimport { refreshSemaphore } from './semaphore/refresh'\nimport { releaseSemaphore } from './semaphore/release'\nimport { LockOptions, RedisClient } from './types'\n\nexport default class RedisSemaphore extends RedisMutex {\n  protected _kind = 'semaphore'\n  protected _limit: number\n\n  constructor(\n    client: RedisClient,\n    key: string,\n    limit: number,\n    options?: LockOptions\n  ) {\n    super(client, key, options)\n    if (!limit) {\n      throw new Error('\"limit\" is required')\n    }\n    if (typeof limit !== 'number') {\n      throw new Error('\"limit\" must be a number')\n    }\n    this._key = `semaphore:${key}`\n    this._limit = limit\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshSemaphore(\n      this._client,\n      this._key,\n      this._limit,\n      this._acquireOptions\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireSemaphore(\n      this._client,\n      this._key,\n      this._limit,\n      this._acquireOptions,\n      abortSignal\n    )\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseSemaphore(this._client, this._key, this._identifier)\n  }\n}\n"
  },
  {
    "path": "src/RedlockMultiSemaphore.ts",
    "content": "import { acquireRedlockMultiSemaphore } from './redlockMultiSemaphore/acquire'\nimport { refreshRedlockMultiSemaphore } from './redlockMultiSemaphore/refresh'\nimport { releaseRedlockMultiSemaphore } from './redlockMultiSemaphore/release'\nimport RedlockSemaphore from './RedlockSemaphore'\nimport { LockOptions, RedisClient } from './types'\n\nexport default class RedlockMultiSemaphore extends RedlockSemaphore {\n  protected _kind = 'redlock-multi-semaphore'\n  protected _permits: number\n\n  constructor(\n    clients: RedisClient[],\n    key: string,\n    limit: number,\n    permits: number,\n    options?: LockOptions\n  ) {\n    super(clients, key, limit, options)\n    if (!permits) {\n      throw new Error('\"permits\" is required')\n    }\n    if (typeof permits !== 'number') {\n      throw new Error('\"permits\" must be a number')\n    }\n    this._permits = permits\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshRedlockMultiSemaphore(\n      this._clients,\n      this._key,\n      this._limit,\n      this._permits,\n      this._acquireOptions\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireRedlockMultiSemaphore(\n      this._clients,\n      this._key,\n      this._limit,\n      this._permits,\n      this._acquireOptions,\n      abortSignal\n    )\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseRedlockMultiSemaphore(\n      this._clients,\n      this._key,\n      this._permits,\n      this._identifier\n    )\n  }\n}\n"
  },
  {
    "path": "src/RedlockMutex.ts",
    "content": "import { Lock } from './Lock'\nimport { defaultTimeoutOptions } from './misc'\nimport { acquireRedlockMutex } from './redlockMutex/acquire'\nimport { refreshRedlockMutex } from './redlockMutex/refresh'\nimport { releaseRedlockMutex } from './redlockMutex/release'\nimport { LockOptions, RedisClient } from './types'\n\nexport default class RedlockMutex extends Lock {\n  protected _kind = 'redlock-mutex'\n  protected _key: string\n  protected _clients: RedisClient[]\n\n  constructor(\n    clients: RedisClient[],\n    key: string,\n    options: LockOptions = defaultTimeoutOptions\n  ) {\n    super(options)\n    if (!clients || !Array.isArray(clients)) {\n      throw new Error('\"clients\" array is required')\n    }\n    if (!key) {\n      throw new Error('\"key\" is required')\n    }\n    if (typeof key !== 'string') {\n      throw new Error('\"key\" must be a string')\n    }\n    this._clients = clients\n    this._key = `mutex:${key}`\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshRedlockMutex(\n      this._clients,\n      this._key,\n      this._identifier,\n      this._acquireOptions.lockTimeout\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireRedlockMutex(\n      this._clients,\n      this._key,\n      this._acquireOptions,\n      abortSignal\n    )\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseRedlockMutex(this._clients, this._key, this._identifier)\n  }\n}\n"
  },
  {
    "path": "src/RedlockSemaphore.ts",
    "content": "import RedlockMutex from './RedlockMutex'\nimport { acquireRedlockSemaphore } from './redlockSemaphore/acquire'\nimport { refreshRedlockSemaphore } from './redlockSemaphore/refresh'\nimport { releaseRedlockSemaphore } from './redlockSemaphore/release'\nimport { LockOptions, RedisClient } from './types'\n\nexport default class RedlockSemaphore extends RedlockMutex {\n  protected _kind = 'redlock-semaphore'\n  protected _limit: number\n\n  constructor(\n    clients: RedisClient[],\n    key: string,\n    limit: number,\n    options?: LockOptions\n  ) {\n    super(clients, key, options)\n    if (!limit) {\n      throw new Error('\"limit\" is required')\n    }\n    if (typeof limit !== 'number') {\n      throw new Error('\"limit\" must be a number')\n    }\n    this._key = `semaphore:${key}`\n    this._limit = limit\n  }\n\n  protected async _refresh(): Promise<boolean> {\n    return await refreshRedlockSemaphore(\n      this._clients,\n      this._key,\n      this._limit,\n      this._acquireOptions\n    )\n  }\n\n  protected async _acquire(abortSignal?: AbortSignal): Promise<boolean> {\n    return await acquireRedlockSemaphore(\n      this._clients,\n      this._key,\n      this._limit,\n      this._acquireOptions,\n      abortSignal\n    )\n  }\n\n  protected async _release(): Promise<void> {\n    await releaseRedlockSemaphore(this._clients, this._key, this._identifier)\n  }\n}\n"
  },
  {
    "path": "src/errors/LostLockError.ts",
    "content": "export default class LostLockError extends Error {}\n"
  },
  {
    "path": "src/errors/TimeoutError.ts",
    "content": "export default class TimeoutError extends Error {}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import MultiSemaphore from './RedisMultiSemaphore'\nimport Mutex from './RedisMutex'\nimport Semaphore from './RedisSemaphore'\nimport RedlockMultiSemaphore from './RedlockMultiSemaphore'\nimport RedlockMutex from './RedlockMutex'\nimport RedlockSemaphore from './RedlockSemaphore'\nimport LostLockError from './errors/LostLockError'\nimport TimeoutError from './errors/TimeoutError'\n\nexport { defaultTimeoutOptions } from './misc'\n\nexport {\n  Mutex,\n  Semaphore,\n  MultiSemaphore,\n  RedlockMutex,\n  RedlockSemaphore,\n  RedlockMultiSemaphore,\n  LostLockError,\n  TimeoutError\n}\n\nexport type { LockLostCallback, TimeoutOptions, LockOptions } from './types'\n"
  },
  {
    "path": "src/misc.ts",
    "content": "import LostLockError from './errors/LostLockError'\n\nexport const defaultTimeoutOptions = {\n  lockTimeout: 10000,\n  acquireTimeout: 10000,\n  acquireAttemptsLimit: Number.POSITIVE_INFINITY,\n  retryInterval: 10\n}\n\nexport function defaultOnLockLost(err: LostLockError): never {\n  throw err\n}\n"
  },
  {
    "path": "src/multiSemaphore/acquire/index.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../../types'\nimport { delay } from '../../utils'\nimport { acquireLua } from './lua'\n\nconst debug = createDebug('redis-semaphore:multi-semaphore:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireSemaphore(\n  client: RedisClient,\n  key: string,\n  limit: number,\n  permits: number,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  let now\n  while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, limit, lockTimeout, 'attempt', attempt)\n    const result = await acquireLua(client, [\n      key,\n      limit,\n      permits,\n      identifier,\n      lockTimeout,\n      now\n    ])\n    debug(key, 'result', typeof result, result)\n    if (result === 1) {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, limit, lockTimeout, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/multiSemaphore/acquire/lua.ts",
    "content": "import { createEval } from '../../utils/index'\n\nexport const acquireLua = createEval<\n  [string, number, number, string, number, number],\n  0 | 1\n>(\n  `\n  local key = KEYS[1]\n  local limit = tonumber(ARGV[1])\n  local permits = tonumber(ARGV[2])\n  local identifier = ARGV[3]\n  local lockTimeout = tonumber(ARGV[4])\n  local now = tonumber(ARGV[5])\n  local expiredTimestamp = now - lockTimeout\n  local args = {}\n\n  redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)\n\n  if (redis.call('zcard', key) + permits) <= limit then\n    for i=0, permits - 1 do\n      table.insert(args, now)\n      table.insert(args, identifier .. '_' .. i)\n    end\n    redis.call('zadd', key, unpack(args))\n    redis.call('pexpire', key, lockTimeout)\n    return 1\n  else\n    return 0\n  end`,\n  1\n)\n"
  },
  {
    "path": "src/multiSemaphore/refresh/index.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../../types'\nimport { refreshLua } from './lua'\n\nconst debug = createDebug('redis-semaphore:multi-semaphore:refresh')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n}\n\nexport async function refreshSemaphore(\n  client: RedisClient,\n  key: string,\n  limit: number,\n  permits: number,\n  options: Options\n): Promise<boolean> {\n  const { identifier, lockTimeout } = options\n  const now = Date.now()\n  debug(key, identifier, now)\n  const result = await refreshLua(client, [\n    key,\n    limit,\n    permits,\n    identifier,\n    lockTimeout,\n    now\n  ])\n  debug('result', typeof result, result)\n  return result === 1\n}\n"
  },
  {
    "path": "src/multiSemaphore/refresh/lua.ts",
    "content": "import { createEval } from '../../utils/index'\n\nexport const refreshLua = createEval<\n  [string, number, number, string, number, number],\n  0 | 1\n>(\n  `\n  local key = KEYS[1]\n  local limit = tonumber(ARGV[1])\n  local permits = tonumber(ARGV[2])\n  local identifier = ARGV[3]\n  local lockTimeout = tonumber(ARGV[4])\n  local now = tonumber(ARGV[5])\n  local expiredTimestamp = now - lockTimeout\n  local args = {}\n\n  redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)\n\n  if redis.call('zscore', key, identifier .. '_0') then\n    for i=0, permits - 1 do\n      table.insert(args, now)\n      table.insert(args, identifier .. '_' .. i)\n    end\n    redis.call('zadd', key, unpack(args))\n    redis.call('pexpire', key, lockTimeout)\n    return 1\n  else\n    return 0\n  end`,\n  1\n)\n"
  },
  {
    "path": "src/multiSemaphore/release/index.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../../types'\nimport { releaseLua } from './lua'\n\nconst debug = createDebug('redis-semaphore:multi-semaphore:release')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  now: number\n}\n\nexport async function releaseSemaphore(\n  client: RedisClient,\n  key: string,\n  permits: number,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier, permits)\n  const result = await releaseLua(client, [key, permits, identifier])\n  debug('result', typeof result, result)\n}\n"
  },
  {
    "path": "src/multiSemaphore/release/lua.ts",
    "content": "import { createEval } from '../../utils/index'\n\nexport const releaseLua = createEval<[string, number, string], number>(\n  `\n  local key = KEYS[1]\n  local permits = tonumber(ARGV[1])\n  local identifier = ARGV[2]\n  local args = {}\n\n  for i=0, permits - 1 do\n    table.insert(args, identifier .. '_' .. i)\n  end\n\n  return redis.call('zrem', key, unpack(args))\n`,\n  1\n)\n"
  },
  {
    "path": "src/mutex/acquire.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../types'\nimport { delay } from '../utils'\n\nconst debug = createDebug('redis-semaphore:mutex:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireMutex(\n  client: RedisClient,\n  key: string,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  while (Date.now() < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, 'attempt', attempt)\n    const result = await client.set(key, identifier, 'PX', lockTimeout, 'NX')\n    debug('result', typeof result, result)\n    if (result === 'OK') {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/mutex/refresh.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../types'\nimport { createEval } from '../utils/index'\n\nconst debug = createDebug('redis-semaphore:mutex:refresh')\n\nexport const expireIfEqualLua = createEval<[string, string, number], 0 | 1>(\n  `\n  local key = KEYS[1]\n  local identifier = ARGV[1]\n  local lockTimeout = ARGV[2]\n\n  local value = redis.call('get', key)\n\n  if value == identifier then\n    redis.call('pexpire', key, lockTimeout)\n    return 1\n  end\n\n  return 0\n  `,\n  1\n)\n\nexport async function refreshMutex(\n  client: RedisClient,\n  key: string,\n  identifier: string,\n  lockTimeout: number\n): Promise<boolean> {\n  debug(key, identifier)\n  const result = await expireIfEqualLua(client, [key, identifier, lockTimeout])\n  debug('result', typeof result, result)\n\n  // support options.stringNumbers\n  return +result === 1\n}\n"
  },
  {
    "path": "src/mutex/release.ts",
    "content": "import createDebug from 'debug'\nimport { createEval } from '../utils/index'\n\nimport type { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:mutex:release')\n\nexport const delIfEqualLua = createEval<[string, string], 0 | 1>(\n  `\n  local key = KEYS[1]\n  local identifier = ARGV[1]\n\n  if redis.call('get', key) == identifier then\n    return redis.call('del', key)\n  end\n\n  return 0\n  `,\n  1\n)\n\nexport async function releaseMutex(\n  client: RedisClient,\n  key: string,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier)\n  const result = await delIfEqualLua(client, [key, identifier])\n  debug('result', typeof result, result)\n}\n"
  },
  {
    "path": "src/redlockMultiSemaphore/acquire.ts",
    "content": "import createDebug from 'debug'\nimport { acquireLua } from '../multiSemaphore/acquire/lua'\nimport { RedisClient } from '../types'\nimport { delay } from '../utils'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nconst debug = createDebug('redis-semaphore:redlock-multi-semaphore:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireRedlockMultiSemaphore(\n  clients: RedisClient[],\n  key: string,\n  limit: number,\n  permits: number,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  const quorum = getQuorum(clients.length)\n  let now: number\n  while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, 'attempt', attempt)\n    const promises = clients.map(client =>\n      acquireLua(client, [key, limit, permits, identifier, lockTimeout, now])\n        .then(result => +result)\n        .catch(() => 0)\n    )\n    const results = await Promise.all(promises)\n    if (results.reduce(smartSum, 0) >= quorum) {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      const promises = clients.map(client =>\n        client.zrem(key, identifier).catch(() => 0)\n      )\n      await Promise.all(promises)\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/redlockMultiSemaphore/refresh.ts",
    "content": "import createDebug from 'debug'\nimport { acquireLua } from '../multiSemaphore/acquire/lua'\nimport { refreshLua } from '../multiSemaphore/refresh/lua'\nimport { releaseLua } from '../multiSemaphore/release/lua'\nimport { RedisClient } from '../types'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nconst debug = createDebug('redis-semaphore:redlock-semaphore:refresh')\n\ninterface Options {\n  identifier: string\n  lockTimeout: number\n}\n\nexport async function refreshRedlockMultiSemaphore(\n  clients: RedisClient[],\n  key: string,\n  limit: number,\n  permits: number,\n  options: Options\n): Promise<boolean> {\n  const { identifier, lockTimeout } = options\n  const now = Date.now()\n  debug(key, identifier, now)\n  const quorum = getQuorum(clients.length)\n  const promises = clients.map(client =>\n    refreshLua(client, [key, limit, permits, identifier, lockTimeout, now])\n      .then(result => +result)\n      .catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n  const refreshedCount = results.reduce(smartSum, 0)\n  if (refreshedCount >= quorum) {\n    debug(key, identifier, 'refreshed')\n    if (refreshedCount < clients.length) {\n      debug(key, identifier, 'try to acquire on failed nodes')\n      const promises = results\n        .reduce<RedisClient[]>((failedClients, result, index) => {\n          if (!result) {\n            failedClients.push(clients[index])\n          }\n          return failedClients\n        }, [])\n        .map(client =>\n          acquireLua(client, [\n            key,\n            limit,\n            permits,\n            identifier,\n            lockTimeout,\n            now\n          ])\n            .then(result => +result)\n            .catch(() => 0)\n        )\n      const acquireResults = await Promise.all(promises)\n      debug(key, identifier, 'acquire on failed nodes results', acquireResults)\n    }\n    return true\n  } else {\n    const promises = clients.map(client =>\n      releaseLua(client, [key, permits, identifier]).catch(() => 0)\n    )\n    await Promise.all(promises)\n    return false\n  }\n}\n"
  },
  {
    "path": "src/redlockMultiSemaphore/release.ts",
    "content": "import createDebug from 'debug'\nimport { releaseLua } from '../multiSemaphore/release/lua'\nimport { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:redlock-mutex:release')\n\nexport async function releaseRedlockMultiSemaphore(\n  clients: RedisClient[],\n  key: string,\n  permits: number,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier)\n  const promises = clients.map(client =>\n    releaseLua(client, [key, permits, identifier]).catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n}\n"
  },
  {
    "path": "src/redlockMutex/acquire.ts",
    "content": "import createDebug from 'debug'\nimport { delIfEqualLua } from '../mutex/release'\nimport { RedisClient } from '../types'\nimport { delay } from '../utils'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nconst debug = createDebug('redis-semaphore:redlock-mutex:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireRedlockMutex(\n  clients: RedisClient[],\n  key: string,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  const quorum = getQuorum(clients.length)\n  while (Date.now() < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, 'attempt', attempt)\n    const promises = clients.map(client =>\n      client\n        .set(key, identifier, 'PX', lockTimeout, 'NX')\n        .then(result => (result === 'OK' ? 1 : 0))\n        .catch(() => 0)\n    )\n    const results = await Promise.all(promises)\n    if (results.reduce(smartSum, 0) >= quorum) {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      const promises = clients.map(client =>\n        delIfEqualLua(client, [key, identifier]).catch(() => 0)\n      )\n      await Promise.all(promises)\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/redlockMutex/refresh.ts",
    "content": "import createDebug from 'debug'\nimport { expireIfEqualLua } from '../mutex/refresh'\nimport { delIfEqualLua } from '../mutex/release'\nimport { RedisClient } from '../types'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nconst debug = createDebug('redis-semaphore:redlock-mutex:refresh')\n\nexport async function refreshRedlockMutex(\n  clients: RedisClient[],\n  key: string,\n  identifier: string,\n  lockTimeout: number\n): Promise<boolean> {\n  debug(key, identifier)\n  const quorum = getQuorum(clients.length)\n  const promises = clients.map(client =>\n    expireIfEqualLua(client, [key, identifier, lockTimeout])\n      .then(result => +result)\n      .catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n  const refreshedCount = results.reduce(smartSum, 0)\n  if (refreshedCount >= quorum) {\n    debug(key, identifier, 'refreshed')\n    if (refreshedCount < clients.length) {\n      debug(key, identifier, 'try to acquire on failed nodes')\n      const promises = results\n        .reduce<RedisClient[]>((failedClients, result, index) => {\n          if (!result) {\n            failedClients.push(clients[index])\n          }\n          return failedClients\n        }, [])\n        .map(client =>\n          client\n            .set(key, identifier, 'PX', lockTimeout, 'NX')\n            .then(result => (result === 'OK' ? 1 : 0))\n            .catch(() => 0)\n        )\n      const acquireResults = await Promise.all(promises)\n      debug(key, identifier, 'acquire on failed nodes results', acquireResults)\n    }\n    return true\n  } else {\n    const promises = clients.map(client =>\n      delIfEqualLua(client, [key, identifier]).catch(() => 0)\n    )\n    await Promise.all(promises)\n    return false\n  }\n}\n"
  },
  {
    "path": "src/redlockMutex/release.ts",
    "content": "import createDebug from 'debug'\nimport { delIfEqualLua } from '../mutex/release'\n\nimport type { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:redlock-mutex:release')\n\nexport async function releaseRedlockMutex(\n  clients: RedisClient[],\n  key: string,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier)\n  const promises = clients.map(client =>\n    delIfEqualLua(client, [key, identifier]).catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n}\n"
  },
  {
    "path": "src/redlockSemaphore/acquire.ts",
    "content": "import createDebug from 'debug'\nimport { acquireLua } from '../semaphore/acquire/lua'\nimport { RedisClient } from '../types'\nimport { delay } from '../utils'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nconst debug = createDebug('redis-semaphore:redlock-semaphore:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireRedlockSemaphore(\n  clients: RedisClient[],\n  key: string,\n  limit: number,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  const quorum = getQuorum(clients.length)\n  let now: number\n  while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, 'attempt', attempt)\n    const promises = clients.map(client =>\n      acquireLua(client, [key, limit, identifier, lockTimeout, now])\n        .then(result => +result)\n        .catch(() => 0)\n    )\n    const results = await Promise.all(promises)\n    if (results.reduce(smartSum, 0) >= quorum) {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      const promises = clients.map(client =>\n        client.zrem(key, identifier).catch(() => 0)\n      )\n      await Promise.all(promises)\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/redlockSemaphore/refresh.ts",
    "content": "import createDebug from 'debug'\nimport { acquireLua } from '../semaphore/acquire/lua'\nimport { refreshLua } from '../semaphore/refresh/lua'\nimport { getQuorum, smartSum } from '../utils/redlock'\n\nimport type { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:redlock-semaphore:refresh')\n\ninterface Options {\n  identifier: string\n  lockTimeout: number\n}\n\nexport async function refreshRedlockSemaphore(\n  clients: RedisClient[],\n  key: string,\n  limit: number,\n  options: Options\n): Promise<boolean> {\n  const { identifier, lockTimeout } = options\n  const now = Date.now()\n  debug(key, identifier, now)\n  const quorum = getQuorum(clients.length)\n  const promises = clients.map(client =>\n    refreshLua(client, [key, limit, identifier, lockTimeout, now])\n      .then(result => +result)\n      .catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n  const refreshedCount = results.reduce(smartSum, 0)\n  if (refreshedCount >= quorum) {\n    debug(key, identifier, 'refreshed')\n    if (refreshedCount < clients.length) {\n      debug(key, identifier, 'try to acquire on failed nodes')\n      const promises = results\n        .reduce<RedisClient[]>((failedClients, result, index) => {\n          if (!result) {\n            failedClients.push(clients[index])\n          }\n          return failedClients\n        }, [])\n        .map(client =>\n          acquireLua(client, [key, limit, identifier, lockTimeout, now])\n            .then(result => +result)\n            .catch(() => 0)\n        )\n      const acquireResults = await Promise.all(promises)\n      debug(key, identifier, 'acquire on failed nodes results', acquireResults)\n    }\n    return true\n  } else {\n    const promises = clients.map(client =>\n      client.zrem(key, identifier).catch(() => 0)\n    )\n    await Promise.all(promises)\n    return false\n  }\n}\n"
  },
  {
    "path": "src/redlockSemaphore/release.ts",
    "content": "import createDebug from 'debug'\n\nimport type { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:redlock-mutex:release')\n\nexport async function releaseRedlockSemaphore(\n  clients: RedisClient[],\n  key: string,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier)\n  const promises = clients.map(client =>\n    client.zrem(key, identifier).catch(() => 0)\n  )\n  const results = await Promise.all(promises)\n  debug('results', results)\n}\n"
  },
  {
    "path": "src/semaphore/acquire/index.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../../types'\nimport { delay } from '../../utils'\nimport { acquireLua } from './lua'\n\nconst debug = createDebug('redis-semaphore:semaphore:acquire')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n\nexport async function acquireSemaphore(\n  client: RedisClient,\n  key: string,\n  limit: number,\n  options: Options,\n  abortSignal?: AbortSignal\n): Promise<boolean> {\n  const {\n    identifier,\n    lockTimeout,\n    acquireTimeout,\n    acquireAttemptsLimit,\n    retryInterval\n  } = options\n  let attempt = 0\n  const end = Date.now() + acquireTimeout\n  let now\n  while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {\n    abortSignal?.throwIfAborted()\n    debug(key, identifier, limit, lockTimeout, 'attempt', attempt)\n    const result = await acquireLua(client, [\n      key,\n      limit,\n      identifier,\n      lockTimeout,\n      now\n    ])\n    debug(key, 'result', typeof result, result)\n    // support options.stringNumbers\n    if (+result === 1) {\n      debug(key, identifier, 'acquired')\n      return true\n    } else {\n      await delay(retryInterval, abortSignal)\n    }\n  }\n  debug(key, identifier, limit, lockTimeout, 'timeout or reach limit')\n  return false\n}\n"
  },
  {
    "path": "src/semaphore/acquire/lua.ts",
    "content": "import { createEval } from '../../utils/index'\n\nexport const acquireLua = createEval<\n  [string, number, string, number, number],\n  0 | 1\n>(\n  `\n  local key = KEYS[1]\n  local limit = tonumber(ARGV[1])\n  local identifier = ARGV[2]\n  local lockTimeout = tonumber(ARGV[3])\n  local now = tonumber(ARGV[4])\n  local expiredTimestamp = now - lockTimeout\n\n  redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)\n\n  if redis.call('zcard', key) < limit then\n    redis.call('zadd', key, now, identifier)\n    redis.call('pexpire', key, lockTimeout)\n    return 1\n  else\n    return 0\n  end`,\n  1\n)\n"
  },
  {
    "path": "src/semaphore/refresh/index.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../../types'\nimport { refreshLua } from './lua'\n\nconst debug = createDebug('redis-semaphore:semaphore:refresh')\n\nexport interface Options {\n  identifier: string\n  lockTimeout: number\n}\n\nexport async function refreshSemaphore(\n  client: RedisClient,\n  key: string,\n  limit: number,\n  options: Options\n): Promise<boolean> {\n  const { identifier, lockTimeout } = options\n  const now = Date.now()\n  debug(key, identifier, now)\n  const result = await refreshLua(client, [\n    key,\n    limit,\n    identifier,\n    lockTimeout,\n    now\n  ])\n  debug('result', typeof result, result)\n  // support options.stringNumbers\n  return +result === 1\n}\n"
  },
  {
    "path": "src/semaphore/refresh/lua.ts",
    "content": "import { createEval } from '../../utils/index'\n\nexport const refreshLua = createEval<\n  [string, number, string, number, number],\n  0 | 1\n>(\n  `\n  local key = KEYS[1]\n  local limit = tonumber(ARGV[1])\n  local identifier = ARGV[2]\n  local lockTimeout = tonumber(ARGV[3])\n  local now = tonumber(ARGV[4])\n  local expiredTimestamp = now - lockTimeout\n\n  redis.call('zremrangebyscore', key, '-inf', expiredTimestamp)\n\n  if redis.call('zscore', key, identifier) then\n    redis.call('zadd', key, now, identifier)\n    redis.call('pexpire', key, lockTimeout)\n    return 1\n  else\n    return 0\n  end`,\n  1\n)\n"
  },
  {
    "path": "src/semaphore/release.ts",
    "content": "import createDebug from 'debug'\nimport { RedisClient } from '../types'\n\nconst debug = createDebug('redis-semaphore:semaphore:release')\n\nexport async function releaseSemaphore(\n  client: RedisClient,\n  key: string,\n  identifier: string\n): Promise<void> {\n  debug(key, identifier)\n  const result = await client.zrem(key, identifier)\n  debug('result', typeof result, result)\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "import LostLockError from './errors/LostLockError'\nimport { Lock } from './Lock'\n\nimport type * as ioredis from 'ioredis'\n\n/**\n * ioredis-like Redis client\n */\nexport type RedisClient = Pick<\n  ioredis.Redis,\n  'eval' | 'evalsha' | 'get' | 'set' | 'zrem'\n> &\n  Partial<Pick<ioredis.Redis, 'options'>>\n\nexport interface LockLostCallback {\n  (this: Lock, err: LostLockError): void\n}\n\nexport interface TimeoutOptions {\n  lockTimeout?: number\n  acquireTimeout?: number\n  acquireAttemptsLimit?: number\n  retryInterval?: number\n  refreshInterval?: number\n}\n\nexport interface LockOptions extends TimeoutOptions {\n  /**\n   * @deprecated Use `identifier` + `acquiredExternally: true` instead. Will be removed in next major release.\n   */\n  externallyAcquiredIdentifier?: string\n\n  /**\n   * @deprecated Provide custom `identifier` instead. Will be removed in next major release.\n   */\n  identifierSuffix?: string\n\n  /**\n   * Identifier of lock. By default is `crypto.randomUUID()`.\n   *\n   * Must be unique between parallel executors otherwise locks with same identifier *can* be treated as the same lock holder.\n   *\n   * Override only if you know what you are doing, see `acquireExternally` option.\n   */\n  identifier?: string\n\n  /**\n   * If `identifier` provided and `acquiredExternally` is `true` then `_refresh` will be used instead of `_acquire` in `.tryAcquire()`/`.acquire()`.\n   *\n   * Useful for lock sharing between processes: acquire in scheduler, refresh and release in handler.\n   */\n  acquiredExternally?: true\n\n  onLockLost?: LockLostCallback\n}\n\nexport interface AcquireOptions {\n  identifier: string\n  lockTimeout: number\n  acquireTimeout: number\n  acquireAttemptsLimit: number\n  retryInterval: number\n}\n"
  },
  {
    "path": "src/utils/createEval.ts",
    "content": "import { createHash } from 'crypto'\nimport createDebug from 'debug'\nimport { RedisClient } from '../types'\nimport { getConnectionName } from './index'\n\nconst debug = createDebug('redis-semaphore:eval')\n\nfunction createSHA1(script: string): string {\n  return createHash('sha1').update(script, 'utf8').digest('hex')\n}\n\nfunction isNoScriptError(err: Error): boolean {\n  return err.toString().indexOf('NOSCRIPT') !== -1\n}\n\nexport default function createEval<Args extends Array<number | string>, Result>(\n  script: string,\n  keysCount: number\n): (client: RedisClient, args: Args) => Promise<Result> {\n  const sha1 = createSHA1(script)\n  debug('creating script:', script, 'sha1:', sha1)\n  return async function optimizedEval(\n    client: RedisClient,\n    args: Args\n  ): Promise<Result> {\n    const connectionName = getConnectionName(client)\n    const evalSHAArgs = [sha1, keysCount, ...args]\n    debug(connectionName, sha1, 'attempt, args:', evalSHAArgs)\n    try {\n      return (await client.evalsha(sha1, keysCount, ...args)) as Promise<Result>\n    } catch (err) {\n      if (err instanceof Error && isNoScriptError(err)) {\n        const evalArgs = [script, keysCount, ...args]\n        debug(connectionName, sha1, 'fallback to eval, args:', evalArgs)\n        return (await client.eval(\n          script,\n          keysCount,\n          ...args\n        )) as Promise<Result>\n      } else {\n        throw err\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "import { RedisClient } from '../types'\nimport createEval from './createEval'\n\nexport { createEval }\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const timeoutId = setTimeout(() => {\n      cleanup();\n      resolve();\n    }, ms);\n\n    const onAbort = (): void => {\n      cleanup();\n      reject(signal!.reason as Error);\n    };\n\n    const cleanup = (): void => {\n      clearTimeout(timeoutId);\n      signal?.removeEventListener('abort', onAbort);\n    };\n\n    if (signal?.aborted) {\n      onAbort()\n    }\n    signal?.addEventListener('abort', onAbort);\n  })\n}\n\nexport function getConnectionName(client: RedisClient): string {\n  const connectionName = client.options?.connectionName\n  return connectionName ? `<${connectionName}>` : '<unknown client>'\n}\n"
  },
  {
    "path": "src/utils/redlock.ts",
    "content": "export function getQuorum(clientCount: number): number {\n  return Math.round((clientCount + 1) / 2)\n}\n\nexport function smartSum(count: number, zeroOrOne: number): number {\n  return count + zeroOrOne\n}\n"
  },
  {
    "path": "test/init.test.ts",
    "content": "import { init, removeAllListeners } from './unhandledRejection'\n\nbefore(() => {\n  init()\n})\n\nafter(() => {\n  removeAllListeners()\n})\n"
  },
  {
    "path": "test/redisClient.ts",
    "content": "import Redis from 'ioredis'\nimport RedisMock from 'ioredis-mock'\nimport { once } from 'node:events'\n\nfunction createClient(num: number) {\n  const serverURL =\n    process.env[`REDIS_URI${num}`] || `redis://127.0.0.1:${6000 + num}`\n  const client = new Redis(serverURL, {\n    connectionName: `client${num}`,\n    lazyConnect: true,\n    enableOfflineQueue: false,\n    autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)\n    maxRetriesPerRequest: 0, // dont retry, fail faster (default is 20)\n\n    // https://github.com/luin/ioredis#auto-reconnect\n    // retryStrategy is a function that will be called when the connection is lost.\n    // The argument times means this is the nth reconnection being made and the return value represents how long (in ms) to wait to reconnect.\n    retryStrategy() {\n      return 100 // for tests we disable increasing timeout\n    }\n  })\n  client.on('error', err => {\n    console.log('Redis client error:', err.message)\n  })\n  return client\n}\n\nfunction createClientMock(num: number) {\n  return new RedisMock(`redis://mock:${4200 + num}`, {\n    connectionName: `client-mock${num}`,\n    lazyConnect: true,\n    enableOfflineQueue: false,\n    autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)\n    maxRetriesPerRequest: 0, // dont retry, fail faster (default is 20)\n\n    // https://github.com/luin/ioredis#auto-reconnect\n    // retryStrategy is a function that will be called when the connection is lost.\n    // The argument times means this is the nth reconnection being made and the return value represents how long (in ms) to wait to reconnect.\n    retryStrategy() {\n      return 100 // for tests we disable increasing timeout\n    }\n  })\n}\n\nexport const client1 = createClient(1)\nexport const client2 = createClient(2)\nexport const client3 = createClient(3)\n\nexport const allClients = [client1, client2, client3]\n\nexport const clientMock1 = createClientMock(1)\nexport const clientMock2 = createClientMock(2)\nexport const clientMock3 = createClientMock(3)\n\nexport const allClientMocks = [clientMock1, clientMock2, clientMock3]\n\nbefore(async () => {\n  await Promise.all(allClients.map(c => c.connect()))\n  await Promise.all(allClientMocks.map(c => c.connect()))\n})\n\nbeforeEach(async () => {\n  await Promise.all(\n    allClients.map(c => {\n      if (c.status !== 'ready') {\n        console.warn(\n          `client ${c.options.connectionName} status = ${c.status}. Wait for ready.`\n        )\n        return once(c, 'ready')\n      }\n      return null\n    })\n  )\n  await Promise.all(allClients.map(c => c.flushdb()))\n  await Promise.all(allClientMocks.map(c => c.flushdb()))\n})\n\nafter(async () => {\n  await Promise.all(allClients.map(c => c.quit()))\n  await Promise.all(allClientMocks.map(c => c.quit()))\n  // allClients.forEach(c => c.disconnect())\n})\n"
  },
  {
    "path": "test/setup.ts",
    "content": "import chai from 'chai'\nimport chaiAsPromised from 'chai-as-promised'\nimport sinonChai from 'sinon-chai'\n\nchai.use(chaiAsPromised)\nchai.use(sinonChai)\n"
  },
  {
    "path": "test/shell.test.ts",
    "content": "import { downRedisServer, upRedisServer } from './shell'\n\ndescribe('TEST UTILS', () => {\n  describe('shell', () => {\n    it('should up redis server', async function () {\n      this.timeout(30000)\n      await upRedisServer(1)\n    })\n    it('should down and up redis servers', async function () {\n      this.timeout(30000)\n      await downRedisServer(1)\n      await upRedisServer(1)\n    })\n  })\n})\n"
  },
  {
    "path": "test/shell.ts",
    "content": "import { exec } from 'child_process'\n\nimport { delay } from '../src/utils/index'\n\nconst LOGGING = !!process.env.LOGSHELL\n\nfunction sh(cmd: string) {\n  return new Promise<void>((resolve, reject) => {\n    const cp = exec(cmd, (err, stdout, stderr) => {\n      if (stdout && LOGGING) {\n        console.log(`[${cp.pid}] stdout:`)\n        console.log(stdout)\n      }\n      if (stderr && LOGGING) {\n        console.log(`[${cp.pid}] stderr:`)\n        console.log(stderr)\n      }\n      if (err) {\n        reject(err)\n      } else {\n        resolve()\n      }\n    })\n    console.log(`[${cp.pid}] ${cmd}`)\n  })\n}\n\nexport async function upRedisServer(num: number) {\n  const port = 6000 + num\n  await sh(\n    `docker compose up -d redis${num} && yarn wait-for --redis redis://127.0.0.1:${port}`\n  )\n}\n\nexport async function downRedisServer(num: number) {\n  const port = 6000 + num\n  await sh(`docker compose stop redis${num}`)\n  let tries = 0\n  while (true) {\n    try {\n      console.log(`wait server${num} shut down... ${++tries}`)\n      await sh(`yarn wait-for --redis redis://127.0.0.1:${port} -c 1`)\n      await delay(100)\n    } catch {\n      break\n    }\n  }\n}\n"
  },
  {
    "path": "test/src/Lock.test.ts",
    "content": "import { LockOptions } from '../../src'\nimport { Lock } from '../../src/Lock'\nimport { delay } from '../../src/utils'\n\ndescribe('Lock', () => {\n  describe('refresh and release race condition', () => {\n    class TestLock extends Lock {\n      protected _kind = 'test-lock'\n      protected _key: string\n      constructor(key: string, options: LockOptions) {\n        super(options)\n        this._key = key\n      }\n      protected async _refresh(): Promise<boolean> {\n        await delay(200)\n        return false\n      }\n      protected async _acquire(): Promise<boolean> {\n        return true\n      }\n      protected async _release(): Promise<void> {}\n    }\n    it('should not throw LostLock error when refresh started but not finished before release happened', async function () {\n      const lock = new TestLock('key', {\n        lockTimeout: 1000,\n        acquireTimeout: 1000,\n        refreshInterval: 50\n      })\n      try {\n        await lock.acquire()\n        await delay(100)\n      } finally {\n        await lock.release()\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedisMultiSemaphore.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport MultiSemaphore from '../../src/RedisMultiSemaphore'\nimport Semaphore from '../../src/RedisSemaphore'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport { client1 as client, clientMock1 as clientMock } from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\ndescribe('MultiSemaphore', () => {\n  it('should fail on invalid arguments', () => {\n    expect(\n      () => new MultiSemaphore(null as unknown as Redis, 'key', 5, 2)\n    ).to.throw('\"client\" is required')\n    expect(() => new MultiSemaphore(client, '', 5, 2)).to.throw(\n      '\"key\" is required'\n    )\n    expect(\n      () => new MultiSemaphore(client, 1 as unknown as string, 5, 2)\n    ).to.throw('\"key\" must be a string')\n    expect(() => new MultiSemaphore(client, 'key', 0, 2)).to.throw(\n      '\"limit\" is required'\n    )\n    expect(\n      () => new MultiSemaphore(client, 'key', '10' as unknown as number, 2)\n    ).to.throw('\"limit\" must be a number')\n    expect(() => new MultiSemaphore(client, 'key', 5, 0)).to.throw(\n      '\"permits\" is required'\n    )\n    expect(\n      () => new MultiSemaphore(client, 'key', 5, '2' as unknown as number)\n    ).to.throw('\"permits\" must be a number')\n  })\n  it('should acquire and release semaphore', async () => {\n    const semaphore1 = new MultiSemaphore(client, 'key', 3, 2)\n    const semaphore2 = new MultiSemaphore(client, 'key', 3, 1)\n    expect(semaphore1.isAcquired).to.be.false\n    expect(semaphore2.isAcquired).to.be.false\n\n    await semaphore1.acquire()\n    expect(semaphore1.isAcquired).to.be.true\n    await semaphore2.acquire()\n    expect(semaphore2.isAcquired).to.be.true\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      semaphore1.identifier + '_0',\n      semaphore1.identifier + '_1',\n      semaphore2.identifier + '_0'\n    ])\n\n    await semaphore1.release()\n    expect(semaphore1.isAcquired).to.be.false\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier + '_0'\n    ])\n    await semaphore2.release()\n    expect(semaphore2.isAcquired).to.be.false\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should reject after timeout', async () => {\n    const semaphore1 = new MultiSemaphore(client, 'key', 3, 3, timeoutOptions)\n    const semaphore2 = new MultiSemaphore(client, 'key', 3, 1, timeoutOptions)\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire()).to.be.rejectedWith(\n      'Acquire multi-semaphore semaphore:key timeout'\n    )\n    await semaphore1.release()\n    expect(await client.get('semaphore:key')).to.be.eql(null)\n  })\n  it('should abort when the given signal is aborted', async () => {\n    const semaphore1 = new MultiSemaphore(client, 'key', 3, 3, timeoutOptions)\n    const semaphore2 = new MultiSemaphore(client, 'key', 3, 1, timeoutOptions)\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await semaphore1.release()\n    expect(await client.get('semaphore:key')).to.be.eql(null)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const semaphore1 = new MultiSemaphore(client, 'key', 3, 2, timeoutOptions)\n    const semaphore2 = new MultiSemaphore(client, 'key', 3, 1, timeoutOptions)\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      semaphore1.identifier + '_0',\n      semaphore1.identifier + '_1',\n      semaphore2.identifier + '_0'\n    ])\n    await semaphore1.release()\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier + '_0'\n    ])\n    await semaphore2.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should stop refreshing lock if stopped', async () => {\n    const semaphore1 = new MultiSemaphore(client, 'key', 3, 2, timeoutOptions)\n    const semaphore2 = new MultiSemaphore(client, 'key', 3, 1, timeoutOptions)\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await semaphore1.stopRefresh()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier + '_0'\n    ])\n    await semaphore2.stopRefresh()\n    await delay(400)\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should acquire maximum LIMIT semaphores', async () => {\n    const s = () =>\n      new MultiSemaphore(client, 'key', 3, 1, {\n        acquireTimeout: 1000,\n        lockTimeout: 50,\n        retryInterval: 10,\n        refreshInterval: 0 // disable refresh\n      })\n    const pr1 = Promise.all([s().acquire(), s().acquire(), s().acquire()])\n    await delay(5)\n    const pr2 = Promise.all([s().acquire(), s().acquire(), s().acquire()])\n    await pr1\n    const ids1 = await client.zrange('semaphore:key', 0, -1)\n    expect(ids1.length).to.be.eql(3)\n    await pr2\n    const ids2 = await client.zrange('semaphore:key', 0, -1)\n    expect(ids2.length).to.be.eql(3)\n    expect(ids2)\n      .to.not.include(ids1[0])\n      .and.not.include(ids1[1])\n      .and.not.include(ids1[2])\n  })\n  it('should support externally acquired semaphore (deprecated interface)', async () => {\n    const externalSemaphore = new MultiSemaphore(client, 'key', 3, 2, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new MultiSemaphore(client, 'key', 3, 2, {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalSemaphore.identifier\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      localSemaphore.identifier + '_0',\n      localSemaphore.identifier + '_1'\n    ])\n    await localSemaphore.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should support externally acquired semaphore', async () => {\n    const externalSemaphore = new MultiSemaphore(client, 'key', 3, 2, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new MultiSemaphore(client, 'key', 3, 2, {\n      ...timeoutOptions,\n      identifier: externalSemaphore.identifier,\n      acquiredExternally: true\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      localSemaphore.identifier + '_0',\n      localSemaphore.identifier + '_1'\n    ])\n    await localSemaphore.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock is lost between refreshes', async () => {\n      const semaphore = new MultiSemaphore(client, 'key', 3, 2, timeoutOptions)\n      await semaphore.acquire()\n      await client.del('semaphore:key')\n      await client.zadd(\n        'semaphore:key',\n        Date.now(),\n        'aaa',\n        Date.now(),\n        'bbb',\n        Date.now(),\n        'ccc'\n      )\n      await delay(200)\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided', async () => {\n      const onLockLostCallback = sinon.spy(function (this: MultiSemaphore) {\n        expect(this.isAcquired).to.be.false\n      })\n      const semaphore = new MultiSemaphore(client, 'key', 3, 2, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await semaphore.acquire()\n      expect(semaphore.isAcquired).to.be.true\n      await client.del('semaphore:key')\n      await client.zadd(\n        'semaphore:key',\n        Date.now(),\n        'aaa',\n        Date.now(),\n        'bbb',\n        Date.now(),\n        'ccc'\n      )\n      await delay(200)\n      expect(semaphore.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  describe('reusable', () => {\n    it('autorefresh enabled', async function () {\n      this.timeout(10000)\n      const semaphore1 = new MultiSemaphore(client, 'key', 4, 2, timeoutOptions)\n      const semaphore2 = new MultiSemaphore(client, 'key', 4, 2, timeoutOptions)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n    })\n\n    it('autorefresh disabled', async () => {\n      const noRefreshOptions = {\n        ...timeoutOptions,\n        refreshInterval: 0,\n        acquireTimeout: 10\n      }\n      const semaphore1 = new MultiSemaphore(\n        client,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n      const semaphore2 = new MultiSemaphore(\n        client,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n      const semaphore3 = new MultiSemaphore(\n        client,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      // [0/2]\n      await semaphore1.acquire()\n      // [1/2]\n      await delay(80)\n      await semaphore2.acquire()\n      // [2/2]\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire multi-semaphore semaphore:key timeout'\n      ) // rejectes after 10ms\n\n      // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)\n      // semaphore1 will expire after 300 - 90 = 210ms\n      await delay(210)\n\n      // [1/2]\n      await semaphore3.acquire()\n    })\n  })\n  describe('Compatibility with Semaphore', () => {\n    it('should work with Semaphore', async () => {\n      const multiSemaphore1 = new MultiSemaphore(\n        client,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      const multiSemaphore2 = new MultiSemaphore(\n        client,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      const semaphore1 = new Semaphore(client, 'key', 3, timeoutOptions)\n      const semaphore2 = new Semaphore(client, 'key', 3, timeoutOptions)\n      await multiSemaphore1.acquire()\n      await semaphore1.acquire()\n      expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n        multiSemaphore1.identifier + '_0',\n        multiSemaphore1.identifier + '_1',\n        semaphore1.identifier\n      ])\n      await expect(multiSemaphore2.acquire()).to.be.rejectedWith(\n        'Acquire multi-semaphore semaphore:key timeout'\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire semaphore semaphore:key timeout'\n      )\n      await multiSemaphore1.release()\n      expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n        semaphore1.identifier\n      ])\n      await semaphore1.release()\n      expect(await client.zcard('semaphore:key')).to.be.eql(0)\n    })\n  })\n  describe('[Node shutdown]', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(async () => {\n      throwUnhandledRejection()\n      await upRedisServer(1)\n    })\n    it('should lost lock when node become alive', async function () {\n      this.timeout(60000)\n      const onLockLostCallback = sinon.spy(function (this: Semaphore) {\n        expect(this.isAcquired).to.be.false\n      })\n      const semaphore1 = new MultiSemaphore(client, 'key', 3, 2, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await semaphore1.acquire()\n\n      await downRedisServer(1)\n      console.log('SHUT DOWN')\n\n      await delay(1000)\n\n      await upRedisServer(1)\n      console.log('ONLINE')\n\n      // semaphore was expired, key was deleted in redis\n      // give refresh mechanism time to detect lock lost\n      // (includes reconnection time)\n      await delay(1000)\n\n      const data1 = await client.zrange('semaphore:key', 0, -1, 'WITHSCORES')\n      // console.log(data)\n      expect(data1).to.be.eql([])\n\n      // lock was not refreshed by semaphore1, so semaphore2 can acquire the lock\n\n      const semaphore2 = new MultiSemaphore(client, 'key', 3, 2, timeoutOptions)\n      await semaphore2.acquire()\n      const data2 = await client.zrange('semaphore:key', 0, -1, 'WITHSCORES')\n      expect(data2).to.include(semaphore2.identifier + '_0')\n      expect(data2).to.include(semaphore2.identifier + '_1')\n\n      await Promise.all([semaphore1.release(), semaphore2.release()])\n    })\n  })\n  describe('ioredis-mock support', () => {\n    it('should acquire and release semaphore', async () => {\n      const semaphore1 = new MultiSemaphore(clientMock, 'key', 3, 2)\n      const semaphore2 = new MultiSemaphore(clientMock, 'key', 3, 1)\n      expect(semaphore1.isAcquired).to.be.false\n      expect(semaphore2.isAcquired).to.be.false\n\n      await semaphore1.acquire()\n      expect(semaphore1.isAcquired).to.be.true\n      await semaphore2.acquire()\n      expect(semaphore2.isAcquired).to.be.true\n      expect(await clientMock.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore1.identifier + '_0',\n        semaphore1.identifier + '_1',\n        semaphore2.identifier + '_0'\n      ])\n\n      await semaphore1.release()\n      expect(semaphore1.isAcquired).to.be.false\n      expect(await clientMock.zrange('semaphore:key', 0, -1)).to.be.eql([\n        semaphore2.identifier + '_0'\n      ])\n      await semaphore2.release()\n      expect(semaphore2.isAcquired).to.be.false\n      expect(await clientMock.zcard('semaphore:key')).to.be.eql(0)\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedisMutex.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport Mutex from '../../src/RedisMutex'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport { client1 as client, clientMock1 as clientMock } from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\ndescribe('Mutex', () => {\n  it('should fail on invalid arguments', () => {\n    expect(() => new Mutex(null as unknown as Redis, 'key')).to.throw(\n      '\"client\" is required'\n    )\n    expect(() => new Mutex(client, '')).to.throw('\"key\" is required')\n    expect(() => new Mutex(client, 1 as unknown as string)).to.throw(\n      '\"key\" must be a string'\n    )\n    expect(() => new Mutex(client, 'key', { identifier: '' })).to.throw(\n      'identifier must be not empty random string'\n    )\n    expect(\n      () => new Mutex(client, 'key', { acquiredExternally: true })\n    ).to.throw('acquiredExternally=true meanless without custom identifier')\n    expect(\n      () =>\n        new Mutex(client, 'key', {\n          externallyAcquiredIdentifier: '123',\n          identifier: '123'\n        })\n    ).to.throw(\n      'Invalid usage. Use custom identifier and acquiredExternally: true'\n    )\n    expect(\n      () =>\n        new Mutex(client, 'key', {\n          externallyAcquiredIdentifier: '123',\n          acquiredExternally: true,\n          identifier: '123'\n        })\n    ).to.throw(\n      'Invalid usage. Use custom identifier and acquiredExternally: true'\n    )\n  })\n  it('should set default options', () => {\n    expect(new Mutex(client, 'key', {})).to.be.ok\n    expect(new Mutex(client, 'key')).to.be.ok\n  })\n  it('should set random UUID as identifier', () => {\n    expect(new Mutex(client, 'key').identifier).to.match(\n      /^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$/\n    )\n  })\n  it('should add identifier suffix', () => {\n    expect(\n      new Mutex(client, 'key', { identifierSuffix: 'abc' }).identifier\n    ).to.match(\n      /^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}-abc$/\n    )\n  })\n  it('should use custom identifier if provided', () => {\n    expect(\n      new Mutex(client, 'key', { identifier: 'abc' }).identifier\n    ).to.be.eql('abc')\n  })\n  it('should acquire and release lock', async () => {\n    const mutex = new Mutex(client, 'key')\n    expect(mutex.isAcquired).to.be.false\n    await mutex.acquire()\n    expect(mutex.isAcquired).to.be.true\n    expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)\n    await mutex.release()\n    expect(mutex.isAcquired).to.be.false\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should reject after timeout', async () => {\n    const mutex1 = new Mutex(client, 'key', timeoutOptions)\n    const mutex2 = new Mutex(client, 'key', timeoutOptions)\n    await mutex1.acquire()\n    await expect(mutex2.acquire()).to.be.rejectedWith(\n      'Acquire mutex mutex:key timeout'\n    )\n    await mutex1.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should abort when the given signal is aborted', async () => {\n    const mutex1 = new Mutex(client, 'key', timeoutOptions)\n    const mutex2 = new Mutex(client, 'key', timeoutOptions)\n    await mutex1.acquire()\n    await expect(mutex2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await mutex1.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should return false for tryAcquire after timeout', async () => {\n    const mutex1 = new Mutex(client, 'key', timeoutOptions)\n    const mutex2 = new Mutex(client, 'key', timeoutOptions)\n    await mutex1.acquire()\n    const result = await mutex2.tryAcquire()\n    expect(result).to.be.false\n    await mutex1.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should return true for successful tryAcquire', async () => {\n    const mutex = new Mutex(client, 'key', timeoutOptions)\n    const result = await mutex.tryAcquire()\n    expect(result).to.be.true\n    await mutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const mutex = new Mutex(client, 'key', timeoutOptions)\n    await mutex.acquire()\n    await delay(400)\n    expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)\n    await mutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should stop refreshing lock every refreshInterval ms if stopped', async () => {\n    const mutex = new Mutex(client, 'key', timeoutOptions)\n    await mutex.acquire()\n    mutex.stopRefresh()\n    await delay(400)\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should not call _refresh if already refreshing', async () => {\n    const mutex = new Mutex(client, 'key', timeoutOptions)\n    let callCount = 0\n    Object.assign(mutex, {\n      _refresh: () =>\n        delay(100).then(() => {\n          callCount++\n          return true\n        })\n    })\n    await mutex.acquire()\n    await delay(400)\n    expect(callCount).to.be.eql(2) // not floor(400/80) = 9\n  })\n\n  it('should support externally acquired mutex (deprecated interface)', async () => {\n    const externalMutex = new Mutex(client, 'key', {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localMutex = new Mutex(client, 'key', {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalMutex.identifier\n    })\n    await externalMutex.acquire()\n    await localMutex.acquire()\n    await delay(400)\n    expect(await client.get('mutex:key')).to.be.eql(localMutex.identifier)\n    await localMutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  it('should support externally acquired mutex', async () => {\n    const externalMutex = new Mutex(client, 'key', {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localMutex = new Mutex(client, 'key', {\n      ...timeoutOptions,\n      identifier: externalMutex.identifier,\n      acquiredExternally: true\n    })\n    await externalMutex.acquire()\n    await localMutex.acquire()\n    await delay(400)\n    expect(await client.get('mutex:key')).to.be.eql(localMutex.identifier)\n    await localMutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock was lost between refreshes (another instance acquired)', async () => {\n      const mutex = new Mutex(client, 'key', timeoutOptions)\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      await client.set('mutex:key', '222') // another instance\n      await delay(200)\n      expect(mutex.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should throw unhandled error if lock was lost between refreshes (lock expired)', async () => {\n      const mutex = new Mutex(client, 'key', timeoutOptions)\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      await client.del('mutex:key') // expired\n      await delay(200)\n      expect(mutex.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided (another instance acquired)', async () => {\n      const onLockLostCallback = sinon.spy(function (this: Mutex) {\n        expect(this.isAcquired).to.be.false\n      })\n      const mutex = new Mutex(client, 'key', {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      await client.set('mutex:key', '222') // another instance\n      await delay(200)\n      expect(mutex.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n    it('should call onLockLost callback if provided (lock expired)', async () => {\n      const onLockLostCallback = sinon.spy(function (this: Mutex) {\n        expect(this.isAcquired).to.be.false\n      })\n      const mutex = new Mutex(client, 'key', {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      await client.del('mutex:key') // expired\n      await delay(200)\n      expect(mutex.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  it('should be reusable', async function () {\n    this.timeout(10000)\n    const mutex = new Mutex(client, 'key', timeoutOptions)\n\n    /* Lifecycle 1 */\n    await mutex.acquire()\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)\n    await mutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(null)\n\n    await delay(300)\n\n    /* Lifecycle 2 */\n    await mutex.acquire()\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)\n    await mutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(null)\n\n    await delay(300)\n\n    /* Lifecycle 3 */\n    await mutex.acquire()\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)\n    await mutex.release()\n    expect(await client.get('mutex:key')).to.be.eql(null)\n    await delay(300)\n    expect(await client.get('mutex:key')).to.be.eql(null)\n  })\n  describe('[Node shutdown]', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(async () => {\n      throwUnhandledRejection()\n      await upRedisServer(1)\n    })\n    it('should lost lock when node become alive', async function () {\n      this.timeout(60000)\n      const onLockLostCallback = sinon.spy(function (this: Mutex) {\n        expect(this.isAcquired).to.be.false\n      })\n      const mutex1 = new Mutex(client, 'key', {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await mutex1.acquire()\n      await downRedisServer(1)\n\n      await delay(1000)\n      // lock expired now\n\n      await upRedisServer(1)\n      // mutex was expired, key was deleted in redis\n      // give refresh mechanism time to detect lock lost\n      // (includes client reconnection time)\n      await delay(1000)\n\n      expect(await client.get('mutex:key')).to.be.eql(null)\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n\n      // lock was not reacquired by mutex1, so mutex2 can acquire the lock\n\n      const mutex2 = new Mutex(client, 'key', timeoutOptions)\n      await mutex2.acquire()\n      expect(await client.get('mutex:key')).to.be.eql(mutex2.identifier)\n\n      await Promise.all([mutex1.release(), mutex2.release()])\n    })\n  })\n  describe('ioredis-mock support', async () => {\n    it('should acquire and release lock', async () => {\n      const mutex = new Mutex(clientMock, 'key')\n      expect(mutex.isAcquired).to.be.false\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      expect(await clientMock.get('mutex:key')).to.be.eql(mutex.identifier)\n      await mutex.release()\n      expect(mutex.isAcquired).to.be.false\n      expect(await clientMock.get('mutex:key')).to.be.eql(null)\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedisSemaphore.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport Semaphore from '../../src/RedisSemaphore'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport { client1 as client, clientMock1 as clientMock } from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\ndescribe('Semaphore', () => {\n  it('should fail on invalid arguments', () => {\n    expect(() => new Semaphore(null as unknown as Redis, 'key', 5)).to.throw(\n      '\"client\" is required'\n    )\n    expect(() => new Semaphore(client, '', 5)).to.throw('\"key\" is required')\n    expect(() => new Semaphore(client, 1 as unknown as string, 5)).to.throw(\n      '\"key\" must be a string'\n    )\n    expect(() => new Semaphore(client, 'key', 0)).to.throw(\n      '\"limit\" is required'\n    )\n    expect(\n      () => new Semaphore(client, 'key', '10' as unknown as number)\n    ).to.throw('\"limit\" must be a number')\n  })\n  it('should acquire and release semaphore', async () => {\n    const semaphore1 = new Semaphore(client, 'key', 2)\n    const semaphore2 = new Semaphore(client, 'key', 2)\n    expect(semaphore1.isAcquired).to.be.false\n    expect(semaphore2.isAcquired).to.be.false\n\n    await semaphore1.acquire()\n    expect(semaphore1.isAcquired).to.be.true\n    await semaphore2.acquire()\n    expect(semaphore2.isAcquired).to.be.true\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      semaphore1.identifier,\n      semaphore2.identifier\n    ])\n\n    await semaphore1.release()\n    expect(semaphore1.isAcquired).to.be.false\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier\n    ])\n    await semaphore2.release()\n    expect(semaphore2.isAcquired).to.be.false\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should reject after timeout', async () => {\n    const semaphore1 = new Semaphore(client, 'key', 1, timeoutOptions)\n    const semaphore2 = new Semaphore(client, 'key', 1, timeoutOptions)\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire()).to.be.rejectedWith(\n      'Acquire semaphore semaphore:key timeout'\n    )\n    await semaphore1.release()\n    expect(await client.get('semaphore:key')).to.be.eql(null)\n  })\n  it('should abort when the given signal is aborted', async () => {\n    const semaphore1 = new Semaphore(client, 'key', 1, timeoutOptions)\n    const semaphore2 = new Semaphore(client, 'key', 1, timeoutOptions)\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await semaphore1.release()\n    expect(await client.get('semaphore:key')).to.be.eql(null)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)\n    const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      semaphore1.identifier,\n      semaphore2.identifier\n    ])\n    await semaphore1.release()\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier\n    ])\n    await semaphore2.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should stop refreshing lock if stopped', async () => {\n    const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)\n    const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await semaphore1.stopRefresh()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([\n      semaphore2.identifier\n    ])\n    await semaphore2.stopRefresh()\n    await delay(400)\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should acquire maximum LIMIT semaphores', async () => {\n    const s = () =>\n      new Semaphore(client, 'key', 3, {\n        acquireTimeout: 1000,\n        lockTimeout: 50,\n        retryInterval: 10,\n        refreshInterval: 0 // disable refresh\n      })\n    const pr1 = Promise.all([s().acquire(), s().acquire(), s().acquire()])\n    await delay(5)\n    const pr2 = Promise.all([s().acquire(), s().acquire(), s().acquire()])\n    await pr1\n    const ids1 = await client.zrange('semaphore:key', 0, -1)\n    expect(ids1.length).to.be.eql(3)\n    await pr2\n    const ids2 = await client.zrange('semaphore:key', 0, -1)\n    expect(ids2.length).to.be.eql(3)\n    expect(ids2)\n      .to.not.include(ids1[0])\n      .and.not.include(ids1[1])\n      .and.not.include(ids1[2])\n  })\n  it('should support externally acquired semaphore (deprecated interface)', async () => {\n    const externalSemaphore = new Semaphore(client, 'key', 3, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new Semaphore(client, 'key', 3, {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalSemaphore.identifier\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      localSemaphore.identifier\n    ])\n    await localSemaphore.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  it('should support externally acquired semaphore', async () => {\n    const externalSemaphore = new Semaphore(client, 'key', 3, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new Semaphore(client, 'key', 3, {\n      ...timeoutOptions,\n      identifier: externalSemaphore.identifier,\n      acquiredExternally: true\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n      localSemaphore.identifier\n    ])\n    await localSemaphore.release()\n    expect(await client.zcard('semaphore:key')).to.be.eql(0)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock is lost between refreshes', async () => {\n      const semaphore = new Semaphore(client, 'key', 3, timeoutOptions)\n      await semaphore.acquire()\n      await client.del('semaphore:key')\n      await client.zadd(\n        'semaphore:key',\n        Date.now(),\n        'aaa',\n        Date.now(),\n        'bbb',\n        Date.now(),\n        'ccc'\n      )\n      await delay(200)\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided', async () => {\n      const onLockLostCallback = sinon.spy(function (this: Semaphore) {\n        expect(this.isAcquired).to.be.false\n      })\n      const semaphore = new Semaphore(client, 'key', 3, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await semaphore.acquire()\n      expect(semaphore.isAcquired).to.be.true\n      await client.del('semaphore:key')\n      await client.zadd(\n        'semaphore:key',\n        Date.now(),\n        'aaa',\n        Date.now(),\n        'bbb',\n        Date.now(),\n        'ccc'\n      )\n      await delay(200)\n      expect(semaphore.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  describe('reusable', () => {\n    it('autorefresh enabled', async () => {\n      const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)\n      const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n    })\n\n    it('autorefresh disabled', async () => {\n      const noRefreshOptions = {\n        ...timeoutOptions,\n        refreshInterval: 0,\n        acquireTimeout: 10\n      }\n      const semaphore1 = new Semaphore(client, 'key', 2, noRefreshOptions)\n      const semaphore2 = new Semaphore(client, 'key', 2, noRefreshOptions)\n      const semaphore3 = new Semaphore(client, 'key', 2, noRefreshOptions)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      // [0/2]\n      await semaphore1.acquire()\n      // [1/2]\n      await delay(80)\n      await semaphore2.acquire()\n      // [2/2]\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire semaphore semaphore:key timeout'\n      ) // rejectes after 10ms\n\n      // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)\n      // semaphore1 will expire after 300 - 90 = 210ms\n      await delay(210)\n\n      // [1/2]\n      await semaphore3.acquire()\n    })\n  })\n  describe('[Node shutdown]', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(async () => {\n      throwUnhandledRejection()\n      await upRedisServer(1)\n    })\n    it('should lost lock when node become alive', async function () {\n      this.timeout(60000)\n      const onLockLostCallbacks = [1, 2, 3].map(() =>\n        sinon.spy(function (this: Semaphore) {\n          expect(this.isAcquired).to.be.false\n        })\n      )\n      const semaphores1 = [1, 2, 3].map(\n        (n, i) =>\n          new Semaphore(client, 'key', 3, {\n            ...timeoutOptions,\n            onLockLost: onLockLostCallbacks[i]\n          })\n      )\n\n      await Promise.all(semaphores1.map(s => s.acquire()))\n\n      await downRedisServer(1)\n      console.log('SHUT DOWN')\n\n      await delay(1000)\n\n      await upRedisServer(1)\n      console.log('ONLINE')\n\n      // semaphore was expired, key was deleted in redis\n      // give refresh mechanism time to detect lock lost\n      // (includes reconnection time)\n      await delay(1000)\n\n      const data = await client.zrange('semaphore:key', 0, -1, 'WITHSCORES')\n      expect(data).to.be.eql([])\n\n      // console.log(data)\n      // expect(data).to.include(semaphore11.identifier)\n      // expect(data).to.include(semaphore12.identifier)\n      // expect(data).to.include(semaphore13.identifier)\n\n      // lock was not reacquired by semaphore1[1-3], so semaphore2 can acquire the lock\n\n      const semaphore2 = new Semaphore(client, 'key', 3, timeoutOptions)\n      await semaphore2.acquire()\n\n      expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore2.identifier\n      ])\n\n      await Promise.all([\n        ...semaphores1.map(s => s.release()),\n        semaphore2.release()\n      ])\n    })\n  })\n  describe('ioredis-mock support', async () => {\n    it('should acquire and release semaphore', async () => {\n      const semaphore1 = new Semaphore(clientMock, 'key', 2)\n      const semaphore2 = new Semaphore(clientMock, 'key', 2)\n      expect(semaphore1.isAcquired).to.be.false\n      expect(semaphore2.isAcquired).to.be.false\n\n      await semaphore1.acquire()\n      expect(semaphore1.isAcquired).to.be.true\n      await semaphore2.acquire()\n      expect(semaphore2.isAcquired).to.be.true\n      expect(await clientMock.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore1.identifier,\n        semaphore2.identifier\n      ])\n\n      await semaphore1.release()\n      expect(semaphore1.isAcquired).to.be.false\n      expect(await clientMock.zrange('semaphore:key', 0, -1)).to.be.eql([\n        semaphore2.identifier\n      ])\n      await semaphore2.release()\n      expect(semaphore2.isAcquired).to.be.false\n      expect(await clientMock.zcard('semaphore:key')).to.be.eql(0)\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedlockMultiSemaphore.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport RedlockMultiSemaphore from '../../src/RedlockMultiSemaphore'\nimport RedlockSemaphore from '../../src/RedlockSemaphore'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport {\n  allClientMocks,\n  allClients,\n  client1,\n  client2,\n  client3\n} from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\nasync function expectZRangeAllEql(key: string, values: string[]) {\n  const results = await Promise.all([\n    client1.zrange(key, 0, -1),\n    client2.zrange(key, 0, -1),\n    client3.zrange(key, 0, -1)\n  ])\n  expect(results).to.be.eql([values, values, values])\n}\n\nasync function expectZRangeAllHaveMembers(key: string, values: string[]) {\n  const results = await Promise.all([\n    client1.zrange(key, 0, -1),\n    client2.zrange(key, 0, -1),\n    client3.zrange(key, 0, -1)\n  ])\n  for (const result of results) {\n    expect(result).to.have.members(values)\n  }\n}\n\nasync function expectZCardAllEql(key: string, count: number) {\n  const results = await Promise.all([\n    client1.zcard(key),\n    client2.zcard(key),\n    client3.zcard(key)\n  ])\n  expect(results).to.be.eql([count, count, count])\n}\n\ndescribe('RedlockMultiSemaphore', () => {\n  it('should fail on invalid arguments', () => {\n    expect(\n      () => new RedlockMultiSemaphore(null as unknown as Redis[], 'key', 5, 2)\n    ).to.throw('\"clients\" array is required')\n    expect(() => new RedlockMultiSemaphore(allClients, '', 5, 2)).to.throw(\n      '\"key\" is required'\n    )\n    expect(\n      () => new RedlockMultiSemaphore(allClients, 1 as unknown as string, 5, 2)\n    ).to.throw('\"key\" must be a string')\n    expect(() => new RedlockMultiSemaphore(allClients, 'key', 0, 2)).to.throw(\n      '\"limit\" is required'\n    )\n    expect(\n      () =>\n        new RedlockMultiSemaphore(\n          allClients,\n          'key',\n          '10' as unknown as number,\n          2\n        )\n    ).to.throw('\"limit\" must be a number')\n    expect(() => new RedlockMultiSemaphore(allClients, 'key', 5, 0)).to.throw(\n      '\"permits\" is required'\n    )\n    expect(\n      () =>\n        new RedlockMultiSemaphore(\n          allClients,\n          'key',\n          5,\n          '2' as unknown as number\n        )\n    ).to.throw('\"permits\" must be a number')\n  })\n  it('should acquire and release semaphore', async () => {\n    const semaphore1 = new RedlockMultiSemaphore(allClients, 'key', 3, 2)\n    const semaphore2 = new RedlockMultiSemaphore(allClients, 'key', 3, 1)\n    expect(semaphore1.isAcquired).to.be.false\n    expect(semaphore2.isAcquired).to.be.false\n\n    await semaphore1.acquire()\n    expect(semaphore1.isAcquired).to.be.true\n    await semaphore2.acquire()\n    expect(semaphore2.isAcquired).to.be.true\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      semaphore1.identifier + '_0',\n      semaphore1.identifier + '_1',\n      semaphore2.identifier + '_0'\n    ])\n\n    await semaphore1.release()\n    expect(semaphore1.isAcquired).to.be.false\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier + '_0'])\n    await semaphore2.release()\n    expect(semaphore2.isAcquired).to.be.false\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should reject after timeout', async () => {\n    const semaphore1 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      3,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire()).to.be.rejectedWith(\n      'Acquire redlock-multi-semaphore semaphore:key timeout'\n    )\n    await semaphore1.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should abort when the given signal is aborted', async () => {\n    const semaphore1 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      3,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await semaphore1.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const semaphore1 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      2,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await delay(400)\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      semaphore1.identifier + '_0',\n      semaphore1.identifier + '_1',\n      semaphore2.identifier + '_0'\n    ])\n    await semaphore1.release()\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier + '_0'])\n    await semaphore2.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should stop refreshing lock if stopped', async () => {\n    const semaphore1 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      2,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    semaphore1.stopRefresh()\n    await delay(400)\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier + '_0'])\n    semaphore2.stopRefresh()\n    await delay(400)\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should acquire maximum LIMIT semaphores', async () => {\n    const s = () =>\n      new RedlockMultiSemaphore(allClients, 'key', 3, 1, {\n        acquireTimeout: 1000,\n        lockTimeout: 50,\n        retryInterval: 10,\n        refreshInterval: 0 // disable refresh\n      })\n    const set1 = [s(), s(), s()]\n    const pr1 = Promise.all(set1.map(sem => sem.acquire()))\n    await delay(5)\n    const set2 = [s(), s(), s()]\n    const pr2 = Promise.all(set2.map(sem => sem.acquire()))\n    await pr1\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      set1[0].identifier + '_0',\n      set1[1].identifier + '_0',\n      set1[2].identifier + '_0'\n    ])\n    await expectZCardAllEql('semaphore:key', 3)\n    await pr2\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      set2[0].identifier + '_0',\n      set2[1].identifier + '_0',\n      set2[2].identifier + '_0'\n    ])\n    await expectZCardAllEql('semaphore:key', 3)\n  })\n  it('should support externally acquired semaphore (deprecated interface)', async () => {\n    const externalSemaphore = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      2,\n      {\n        ...timeoutOptions,\n        refreshInterval: 0\n      }\n    )\n    const localSemaphore = new RedlockMultiSemaphore(allClients, 'key', 3, 2, {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalSemaphore.identifier\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      localSemaphore.identifier + '_0',\n      localSemaphore.identifier + '_1'\n    ])\n    await localSemaphore.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should support externally acquired semaphore', async () => {\n    const externalSemaphore = new RedlockMultiSemaphore(\n      allClients,\n      'key',\n      3,\n      2,\n      {\n        ...timeoutOptions,\n        refreshInterval: 0\n      }\n    )\n    const localSemaphore = new RedlockMultiSemaphore(allClients, 'key', 3, 2, {\n      ...timeoutOptions,\n      identifier: externalSemaphore.identifier,\n      acquiredExternally: true\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      localSemaphore.identifier + '_0',\n      localSemaphore.identifier + '_1'\n    ])\n    await localSemaphore.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock is lost between refreshes', async () => {\n      const semaphore = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      await semaphore.acquire()\n      await Promise.all(allClients.map(client => client.del('semaphore:key')))\n      await Promise.all(\n        allClients.map(client =>\n          client.zadd(\n            'semaphore:key',\n            Date.now(),\n            'aaa',\n            Date.now(),\n            'bbb',\n            Date.now(),\n            'ccc'\n          )\n        )\n      )\n      await delay(200)\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided', async () => {\n      const onLockLostCallback = sinon.spy(function (\n        this: RedlockMultiSemaphore\n      ) {\n        expect(this.isAcquired).to.be.false\n      })\n      const semaphore = new RedlockMultiSemaphore(allClients, 'key', 3, 2, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await semaphore.acquire()\n      expect(semaphore.isAcquired).to.be.true\n      await Promise.all(allClients.map(client => client.del('semaphore:key')))\n      await Promise.all(\n        allClients.map(client =>\n          client.zadd(\n            'semaphore:key',\n            Date.now(),\n            'aaa',\n            Date.now(),\n            'bbb',\n            Date.now(),\n            'ccc'\n          )\n        )\n      )\n      await delay(200)\n      expect(semaphore.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  describe('reusable', () => {\n    it('autorefresh enabled', async function () {\n      this.timeout(10000)\n      const semaphore1 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        4,\n        2,\n        timeoutOptions\n      )\n      const semaphore2 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        4,\n        2,\n        timeoutOptions\n      )\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n    })\n\n    it('autorefresh disabled', async () => {\n      const noRefreshOptions = {\n        ...timeoutOptions,\n        refreshInterval: 0,\n        acquireTimeout: 10\n      }\n      const semaphore1 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n      const semaphore2 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n      const semaphore3 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        4,\n        2,\n        noRefreshOptions\n      )\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(300)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(300)\n\n      // [0/2]\n      await semaphore1.acquire()\n      // [1/2]\n      await delay(80)\n      await semaphore2.acquire()\n      // [2/2]\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire redlock-multi-semaphore semaphore:key timeout'\n      ) // rejectes after 10ms\n\n      // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)\n      // semaphore1 will expire after 300 - 90 = 210ms\n      await delay(210)\n\n      // [1/2]\n      await semaphore3.acquire()\n    })\n  })\n  describe('Compatibility with Semaphore', () => {\n    it('should work with Semaphore', async () => {\n      const multiSemaphore1 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      const multiSemaphore2 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      const semaphore1 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await multiSemaphore1.acquire()\n      await semaphore1.acquire()\n      await expectZRangeAllHaveMembers('semaphore:key', [\n        multiSemaphore1.identifier + '_0',\n        multiSemaphore1.identifier + '_1',\n        semaphore1.identifier\n      ])\n      await expect(multiSemaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-multi-semaphore semaphore:key timeout'\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n      await multiSemaphore1.release()\n      await expectZRangeAllEql('semaphore:key', [semaphore1.identifier])\n      await semaphore1.release()\n      await expectZCardAllEql('semaphore:key', 0)\n    })\n  })\n  describe('[Node shutdown]', () => {\n    afterEach(async () => {\n      await Promise.all([upRedisServer(1), upRedisServer(2), upRedisServer(3)])\n    })\n    it('should handle server shutdown if quorum is alive', async function () {\n      this.timeout(60000)\n      const semaphore11 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        2,\n        timeoutOptions\n      )\n      const semaphore12 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        1,\n        timeoutOptions\n      )\n      await Promise.all([semaphore11.acquire(), semaphore12.acquire()])\n\n      // <Server1Failure>\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await delay(1000)\n\n      // lock survive in server2 and server3\n      // semaphore2 will NOT be able to acquire the lock\n\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server1 has expired now\n\n      await upRedisServer(1)\n      console.log('ONLINE 1')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client1.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier + '_0',\n        semaphore11.identifier + '_1',\n        semaphore12.identifier + '_0'\n      ])\n      // </Server1Failure>\n\n      // <Server2Failure>\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      // lock survive in server1 and server3\n      // semaphore3 will NOT be able to acquire the lock\n\n      const semaphore3 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server2 has expired now\n\n      await upRedisServer(2)\n      console.log('ONLINE 2')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client2.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier + '_0',\n        semaphore11.identifier + '_1',\n        semaphore12.identifier + '_0'\n      ])\n      // </Server2Failure>\n\n      // <Server3Failure>\n      await downRedisServer(3)\n      console.log('SHUT DOWN 3')\n\n      await delay(1000)\n\n      // lock survive in server1 and server2\n      // semaphore4 will NOT be able to acquire the lock\n\n      const semaphore4 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore4.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server1 has expired now\n\n      await upRedisServer(3)\n      console.log('ONLINE 3')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client3.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier + '_0',\n        semaphore11.identifier + '_1',\n        semaphore12.identifier + '_0'\n      ])\n      // </Server3Failure>\n\n      await Promise.all([semaphore11.release(), semaphore12.release()])\n    })\n    it('should fail and release if quorum become dead', async function () {\n      this.timeout(60000)\n      const onLockLostCallbacks = [1, 2].map(() =>\n        sinon.spy(function (this: RedlockMultiSemaphore) {\n          expect(this.isAcquired).to.be.false\n        })\n      )\n\n      const semaphore11 = new RedlockMultiSemaphore(allClients, 'key', 3, 2, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallbacks[0]\n      })\n      const semaphore12 = new RedlockMultiSemaphore(allClients, 'key', 3, 1, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallbacks[1]\n      })\n      await Promise.all([semaphore11.acquire(), semaphore12.acquire()])\n\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      for (const lostCb of onLockLostCallbacks) {\n        expect(lostCb).to.be.called\n        expect(lostCb.firstCall.firstArg instanceof LostLockError).to.be.true\n      }\n\n      // released lock on server3\n      expect(await client3.zrange('semaphore:key', 0, -1)).to.be.eql([])\n\n      // semaphore2 will NOT be able to acquire the lock\n\n      const semaphore2 = new RedlockMultiSemaphore(\n        allClients,\n        'key',\n        3,\n        1,\n        timeoutOptions\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-multi-semaphore semaphore:key timeout'\n      )\n    })\n  })\n  describe('ioredis-mock support', () => {\n    it('should acquire and release semaphore', async () => {\n      const semaphore1 = new RedlockMultiSemaphore(allClientMocks, 'key', 3, 2)\n      const semaphore2 = new RedlockMultiSemaphore(allClientMocks, 'key', 3, 1)\n      expect(semaphore1.isAcquired).to.be.false\n      expect(semaphore2.isAcquired).to.be.false\n\n      await semaphore1.acquire()\n      expect(semaphore1.isAcquired).to.be.true\n      await semaphore2.acquire()\n      expect(semaphore2.isAcquired).to.be.true\n\n      await semaphore1.release()\n      expect(semaphore1.isAcquired).to.be.false\n      await semaphore2.release()\n      expect(semaphore2.isAcquired).to.be.false\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedlockMutex.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport RedlockMutex from '../../src/RedlockMutex'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport {\n  allClientMocks,\n  allClients,\n  client1,\n  client2,\n  client3\n} from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\nasync function expectGetAll(\n  key: string,\n  value: string | null,\n  clients = allClients\n) {\n  await expect(\n    Promise.all([clients[0].get(key), clients[1].get(key), clients[2].get(key)])\n  ).to.become([value, value, value])\n}\n\ndescribe('RedlockMutex', () => {\n  it('should fail on invalid arguments', () => {\n    expect(() => new RedlockMutex(null as unknown as Redis[], 'key')).to.throw(\n      '\"clients\" array is required'\n    )\n    expect(() => new RedlockMutex(allClients, '')).to.throw('\"key\" is required')\n    expect(() => new RedlockMutex(allClients, 1 as unknown as string)).to.throw(\n      '\"key\" must be a string'\n    )\n  })\n  it('should acquire and release lock', async () => {\n    const mutex = new RedlockMutex(allClients, 'key')\n    expect(mutex.isAcquired).to.be.false\n\n    await mutex.acquire()\n    expect(mutex.isAcquired).to.be.true\n    await expectGetAll('mutex:key', mutex.identifier)\n\n    await mutex.release()\n    expect(mutex.isAcquired).to.be.false\n    await expectGetAll('mutex:key', null)\n  })\n  it('should reject after timeout', async () => {\n    const mutex1 = new RedlockMutex(allClients, 'key', timeoutOptions)\n    const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)\n    await mutex1.acquire()\n    await expect(mutex2.acquire()).to.be.rejectedWith(\n      'Acquire redlock-mutex mutex:key timeout'\n    )\n    await mutex1.release()\n    await expectGetAll('mutex:key', null)\n  })\n  it('The operation was aborted due to timeout', async () => {\n    const mutex1 = new RedlockMutex(allClients, 'key', timeoutOptions)\n    const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)\n    await mutex1.acquire()\n    await expect(mutex2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await mutex1.release()\n    await expectGetAll('mutex:key', null)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)\n    await mutex.acquire()\n    await delay(400)\n    await expectGetAll('mutex:key', mutex.identifier)\n    await mutex.release()\n    await expectGetAll('mutex:key', null)\n  })\n  it('should stop refreshing if stopped', async () => {\n    const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)\n    await mutex.acquire()\n    mutex.stopRefresh()\n    await delay(400)\n    await expectGetAll('mutex:key', null)\n  })\n  it('should support externally acquired mutex (deprecated interface)', async () => {\n    const externalMutex = new RedlockMutex(allClients, 'key', {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localMutex = new RedlockMutex(allClients, 'key', {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalMutex.identifier\n    })\n    await externalMutex.acquire()\n    await localMutex.acquire()\n    await delay(400)\n    await expectGetAll('mutex:key', localMutex.identifier)\n    await localMutex.release()\n    await expectGetAll('mutex:key', null)\n  })\n  it('should support externally acquired mutex', async () => {\n    const externalMutex = new RedlockMutex(allClients, 'key', {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localMutex = new RedlockMutex(allClients, 'key', {\n      ...timeoutOptions,\n      identifier: externalMutex.identifier,\n      acquiredExternally: true\n    })\n    await externalMutex.acquire()\n    await localMutex.acquire()\n    await delay(400)\n    await expectGetAll('mutex:key', localMutex.identifier)\n    await localMutex.release()\n    await expectGetAll('mutex:key', null)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock is lost between refreshes', async () => {\n      const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await mutex.acquire()\n      await Promise.all([\n        client1.set('mutex:key', '222'), // another instance\n        client2.set('mutex:key', '222'), // another instance\n        client3.set('mutex:key', '222') // another instance\n      ])\n      await delay(200)\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided', async () => {\n      const onLockLostCallback = sinon.spy(function (this: RedlockMutex) {\n        expect(this.isAcquired).to.be.false\n      })\n      const mutex = new RedlockMutex(allClients, 'key', {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await mutex.acquire()\n      expect(mutex.isAcquired).to.be.true\n      await Promise.all([\n        client1.set('mutex:key', '222'), // another instance\n        client2.set('mutex:key', '222'), // another instance\n        client3.set('mutex:key', '222') // another instance\n      ])\n      await delay(200)\n      expect(mutex.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  it('should be reusable', async () => {\n    const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)\n\n    /* Lifecycle 1 */\n    await mutex.acquire()\n    await delay(100)\n    await expectGetAll('mutex:key', mutex.identifier)\n    await mutex.release()\n    await expectGetAll('mutex:key', null)\n    await delay(100)\n    await expectGetAll('mutex:key', null)\n\n    await delay(100)\n\n    /* Lifecycle 2 */\n    await mutex.acquire()\n    await delay(100)\n    await expectGetAll('mutex:key', mutex.identifier)\n    await mutex.release()\n    await expectGetAll('mutex:key', null)\n    await delay(100)\n    await expectGetAll('mutex:key', null)\n\n    await delay(100)\n\n    /* Lifecycle 3 */\n    await mutex.acquire()\n    await delay(100)\n    await expectGetAll('mutex:key', mutex.identifier)\n    await mutex.release()\n    await expectGetAll('mutex:key', null)\n    await delay(100)\n    await expectGetAll('mutex:key', null)\n  })\n  describe('[Node shutdown]', () => {\n    afterEach(async () => {\n      await Promise.all([upRedisServer(1), upRedisServer(2), upRedisServer(3)])\n    })\n    it('should handle server shutdown if quorum is alive', async function () {\n      this.timeout(60000)\n      const mutex1 = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await mutex1.acquire()\n\n      // <Server1Failure>\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await delay(1000)\n\n      // lock survive in server2 and server3\n      // mutex2 will NOT be able to acquire the lock\n\n      const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await expect(mutex2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-mutex mutex:key timeout'\n      )\n\n      // key in server1 has expired now\n\n      await upRedisServer(1)\n      console.log('ONLINE 1')\n\n      // let mutex1 to refresh lock on server1\n      await delay(1000)\n      expect(await client1.get('mutex:key')).to.be.eql(mutex1.identifier)\n      // </Server1Failure>\n\n      // <Server2Failure>\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      // lock survive in server1 and server3\n      // mutex3 will NOT be able to acquire the lock\n\n      const mutex3 = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await expect(mutex3.acquire()).to.be.rejectedWith(\n        'Acquire redlock-mutex mutex:key timeout'\n      )\n\n      // key in server2 has expired now\n\n      await upRedisServer(2)\n      console.log('ONLINE 2')\n\n      // let mutex1 to refresh lock on server2\n      await delay(1000)\n      expect(await client2.get('mutex:key')).to.be.eql(mutex1.identifier)\n      // </Server2Failure>\n\n      // <Server3Failure>\n      await downRedisServer(3)\n      console.log('SHUT DOWN 3')\n\n      await delay(1000)\n\n      // lock survive in server1 and server2\n      // mutex4 will NOT be able to acquire the lock\n\n      const mutex4 = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await expect(mutex4.acquire()).to.be.rejectedWith(\n        'Acquire redlock-mutex mutex:key timeout'\n      )\n\n      // key in server3 has expired now\n\n      await upRedisServer(3)\n      console.log('ONLINE 3')\n\n      // let mutex1 to refresh lock on server3\n      await delay(1000)\n      expect(await client3.get('mutex:key')).to.be.eql(mutex1.identifier)\n      // </Server3Failure>\n\n      await mutex1.release()\n    })\n    it('should fail and release when quorum is become dead', async function () {\n      this.timeout(60000)\n      const onLockLostCallback = sinon.spy(function (this: RedlockMutex) {\n        expect(this.isAcquired).to.be.false\n      })\n      const mutex1 = new RedlockMutex(allClients, 'key', {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await mutex1.acquire()\n\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n\n      // released lock on server3\n      expect(await client3.get('mutex:key')).to.be.eql(null)\n\n      // mutex2 will NOT be able to acquire the lock cause quorum is dead\n\n      const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)\n      await expect(mutex2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-mutex mutex:key timeout'\n      )\n    })\n  })\n  describe('ioredis-mock support', () => {\n    it('should acquire and release lock', async () => {\n      const mutex = new RedlockMutex(allClientMocks, 'key')\n      expect(mutex.isAcquired).to.be.false\n\n      await mutex.acquire()\n      console.log('acquired!')\n      expect(mutex.isAcquired).to.be.true\n      await expectGetAll('mutex:key', mutex.identifier, allClientMocks)\n\n      await mutex.release()\n      expect(mutex.isAcquired).to.be.false\n      await expectGetAll('mutex:key', null, allClientMocks)\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/RedlockSemaphore.test.ts",
    "content": "import { expect } from 'chai'\nimport { Redis } from 'ioredis'\nimport sinon from 'sinon'\nimport LostLockError from '../../src/errors/LostLockError'\nimport RedlockSemaphore from '../../src/RedlockSemaphore'\nimport { TimeoutOptions } from '../../src/types'\nimport { delay } from '../../src/utils/index'\nimport {\n  allClientMocks,\n  allClients,\n  client1,\n  client2,\n  client3\n} from '../redisClient'\nimport { downRedisServer, upRedisServer } from '../shell'\nimport {\n  catchUnhandledRejection,\n  throwUnhandledRejection,\n  unhandledRejectionSpy\n} from '../unhandledRejection'\n\nconst timeoutOptions: TimeoutOptions = {\n  lockTimeout: 300,\n  acquireTimeout: 100,\n  refreshInterval: 80,\n  retryInterval: 10\n}\n\nasync function expectZRangeAllEql(key: string, values: string[]) {\n  const results = await Promise.all([\n    client1.zrange(key, 0, -1),\n    client2.zrange(key, 0, -1),\n    client3.zrange(key, 0, -1)\n  ])\n  expect(results).to.be.eql([values, values, values])\n}\n\nasync function expectZRangeAllHaveMembers(key: string, values: string[]) {\n  const results = await Promise.all([\n    client1.zrange(key, 0, -1),\n    client2.zrange(key, 0, -1),\n    client3.zrange(key, 0, -1)\n  ])\n  for (const result of results) {\n    expect(result).to.have.members(values)\n  }\n}\n\nasync function expectZCardAllEql(key: string, count: number) {\n  const results = await Promise.all([\n    client1.zcard(key),\n    client2.zcard(key),\n    client3.zcard(key)\n  ])\n  expect(results).to.be.eql([count, count, count])\n}\n\ndescribe('RedlockSemaphore', () => {\n  it('should fail on invalid arguments', () => {\n    expect(\n      () => new RedlockSemaphore(null as unknown as Redis[], 'key', 5)\n    ).to.throw('\"clients\" array is required')\n    expect(() => new RedlockSemaphore(allClients, '', 5)).to.throw(\n      '\"key\" is required'\n    )\n    expect(\n      () => new RedlockSemaphore(allClients, 1 as unknown as string, 5)\n    ).to.throw('\"key\" must be a string')\n    expect(() => new RedlockSemaphore(allClients, 'key', 0)).to.throw(\n      '\"limit\" is required'\n    )\n    expect(\n      () => new RedlockSemaphore(allClients, 'key', '10' as unknown as number)\n    ).to.throw('\"limit\" must be a number')\n  })\n  it('should acquire and release semaphore', async () => {\n    const semaphore1 = new RedlockSemaphore(allClients, 'key', 2)\n    const semaphore2 = new RedlockSemaphore(allClients, 'key', 2)\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      semaphore1.identifier,\n      semaphore2.identifier\n    ])\n    await semaphore1.release()\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])\n    await semaphore2.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should reject after timeout', async () => {\n    const semaphore1 = new RedlockSemaphore(\n      allClients,\n      'key',\n      1,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockSemaphore(\n      allClients,\n      'key',\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire()).to.be.rejectedWith(\n      'Acquire redlock-semaphore semaphore:key timeout'\n    )\n    await semaphore1.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('The operation was aborted due to timeout', async () => {\n    const semaphore1 = new RedlockSemaphore(\n      allClients,\n      'key',\n      1,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockSemaphore(\n      allClients,\n      'key',\n      1,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await expect(semaphore2.acquire(AbortSignal.timeout(10))).to.be.rejectedWith(\n      'The operation was aborted due to timeout'\n    )\n    await semaphore1.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should refresh lock every refreshInterval ms until release', async () => {\n    const semaphore1 = new RedlockSemaphore(\n      allClients,\n      'key',\n      2,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockSemaphore(\n      allClients,\n      'key',\n      2,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    await delay(400)\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      semaphore1.identifier,\n      semaphore2.identifier\n    ])\n    await semaphore1.release()\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])\n    await semaphore2.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should stop refreshing lock if stopped', async () => {\n    const semaphore1 = new RedlockSemaphore(\n      allClients,\n      'key',\n      2,\n      timeoutOptions\n    )\n    const semaphore2 = new RedlockSemaphore(\n      allClients,\n      'key',\n      2,\n      timeoutOptions\n    )\n    await semaphore1.acquire()\n    await semaphore2.acquire()\n    semaphore1.stopRefresh()\n    await delay(400)\n    await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])\n    semaphore2.stopRefresh()\n    await delay(400)\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should acquire maximum LIMIT semaphores', async () => {\n    const s = () =>\n      new RedlockSemaphore(allClients, 'key', 3, {\n        acquireTimeout: 1000,\n        lockTimeout: 50,\n        retryInterval: 10,\n        refreshInterval: 0 // disable refresh\n      })\n    const set1 = [s(), s(), s()]\n    const pr1 = Promise.all(set1.map(sem => sem.acquire()))\n    await delay(5)\n    const set2 = [s(), s(), s()]\n    const pr2 = Promise.all(set2.map(sem => sem.acquire()))\n    await pr1\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      set1[0].identifier,\n      set1[1].identifier,\n      set1[2].identifier\n    ])\n    await expectZCardAllEql('semaphore:key', 3)\n    await pr2\n    await expectZRangeAllHaveMembers('semaphore:key', [\n      set2[0].identifier,\n      set2[1].identifier,\n      set2[2].identifier\n    ])\n    await expectZCardAllEql('semaphore:key', 3)\n  })\n  it('should support externally acquired semaphore (deprecated interface)', async () => {\n    const externalSemaphore = new RedlockSemaphore(allClients, 'key', 3, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new RedlockSemaphore(allClients, 'key', 3, {\n      ...timeoutOptions,\n      externallyAcquiredIdentifier: externalSemaphore.identifier\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    await expectZRangeAllEql('semaphore:key', [localSemaphore.identifier])\n    await localSemaphore.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  it('should support externally acquired semaphore', async () => {\n    const externalSemaphore = new RedlockSemaphore(allClients, 'key', 3, {\n      ...timeoutOptions,\n      refreshInterval: 0\n    })\n    const localSemaphore = new RedlockSemaphore(allClients, 'key', 3, {\n      ...timeoutOptions,\n      identifier: externalSemaphore.identifier,\n      acquiredExternally: true\n    })\n    await externalSemaphore.acquire()\n    await localSemaphore.acquire()\n    await delay(400)\n    await expectZRangeAllEql('semaphore:key', [localSemaphore.identifier])\n    await localSemaphore.release()\n    await expectZCardAllEql('semaphore:key', 0)\n  })\n  describe('lost lock case', () => {\n    beforeEach(() => {\n      catchUnhandledRejection()\n    })\n    afterEach(() => {\n      throwUnhandledRejection()\n    })\n    it('should throw unhandled error if lock is lost between refreshes', async () => {\n      const semaphore = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await semaphore.acquire()\n      await Promise.all(allClients.map(client => client.del('semaphore:key')))\n      await Promise.all(\n        allClients.map(client =>\n          client.zadd(\n            'semaphore:key',\n            Date.now(),\n            'aaa',\n            Date.now(),\n            'bbb',\n            Date.now(),\n            'ccc'\n          )\n        )\n      )\n      await delay(200)\n      expect(unhandledRejectionSpy).to.be.called\n      expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)\n        .to.be.true\n    })\n    it('should call onLockLost callback if provided', async () => {\n      const onLockLostCallback = sinon.spy(function (this: RedlockSemaphore) {\n        expect(this.isAcquired).to.be.false\n      })\n      const semaphore = new RedlockSemaphore(allClients, 'key', 3, {\n        ...timeoutOptions,\n        onLockLost: onLockLostCallback\n      })\n      await semaphore.acquire()\n      expect(semaphore.isAcquired).to.be.true\n      await Promise.all(allClients.map(client => client.del('semaphore:key')))\n      await Promise.all(\n        allClients.map(client =>\n          client.zadd(\n            'semaphore:key',\n            Date.now(),\n            'aaa',\n            Date.now(),\n            'bbb',\n            Date.now(),\n            'ccc'\n          )\n        )\n      )\n      await delay(200)\n      expect(semaphore.isAcquired).to.be.false\n      expect(unhandledRejectionSpy).to.not.called\n      expect(onLockLostCallback).to.be.called\n      expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to\n        .be.true\n    })\n  })\n  describe('reusable', () => {\n    it('autorefresh enabled', async () => {\n      const semaphore1 = new RedlockSemaphore(\n        allClients,\n        'key',\n        2,\n        timeoutOptions\n      )\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        2,\n        timeoutOptions\n      )\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(100)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(100)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(100)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(100)\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(100)\n      await semaphore1.release()\n      await semaphore2.release()\n    })\n\n    it('autorefresh disabled', async () => {\n      const noRefreshOptions = {\n        ...timeoutOptions,\n        refreshInterval: 0,\n        acquireTimeout: 10\n      }\n      const semaphore1 = new RedlockSemaphore(\n        allClients,\n        'key',\n        2,\n        noRefreshOptions\n      )\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        2,\n        noRefreshOptions\n      )\n      const semaphore3 = new RedlockSemaphore(\n        allClients,\n        'key',\n        2,\n        noRefreshOptions\n      )\n\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await delay(100)\n      await semaphore1.release()\n      await semaphore2.release()\n\n      await delay(100)\n\n      // [0/2]\n      await semaphore1.acquire()\n      // [1/2]\n      await delay(80)\n      await semaphore2.acquire()\n      // [2/2]\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      ) // rejectes after 10ms\n\n      // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)\n      // semaphore1 will expire after 300 - 90 = 210ms\n      await delay(210)\n\n      // [1/2]\n      await semaphore3.acquire()\n    })\n  })\n  describe('[Node shutdown]', () => {\n    afterEach(async () => {\n      await Promise.all([upRedisServer(1), upRedisServer(2), upRedisServer(3)])\n    })\n    it('should handle server shutdown if quorum is alive', async function () {\n      this.timeout(60000)\n      const semaphore11 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      const semaphore12 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      const semaphore13 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await Promise.all([\n        semaphore11.acquire(),\n        semaphore12.acquire(),\n        semaphore13.acquire()\n      ])\n\n      // <Server1Failure>\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await delay(1000)\n\n      // lock survive in server2 and server3\n      // semaphore2 will NOT be able to acquire the lock\n\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server1 has expired now\n\n      await upRedisServer(1)\n      console.log('ONLINE 1')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client1.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier,\n        semaphore12.identifier,\n        semaphore13.identifier\n      ])\n      // </Server1Failure>\n\n      // <Server2Failure>\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      // lock survive in server1 and server3\n      // semaphore3 will NOT be able to acquire the lock\n\n      const semaphore3 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore3.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server2 has expired now\n\n      await upRedisServer(2)\n      console.log('ONLINE 2')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client2.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier,\n        semaphore12.identifier,\n        semaphore13.identifier\n      ])\n      // </Server2Failure>\n\n      // <Server3Failure>\n      await downRedisServer(3)\n      console.log('SHUT DOWN 3')\n\n      await delay(1000)\n\n      // lock survive in server1 and server2\n      // semaphore4 will NOT be able to acquire the lock\n\n      const semaphore4 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore4.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n\n      // key in server1 has expired now\n\n      await upRedisServer(3)\n      console.log('ONLINE 3')\n\n      // let semaphore1[1-3] to refresh lock on server1\n      await delay(1000)\n      expect(await client3.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphore11.identifier,\n        semaphore12.identifier,\n        semaphore13.identifier\n      ])\n      // </Server3Failure>\n\n      await Promise.all([\n        semaphore11.release(),\n        semaphore12.release(),\n        semaphore13.release()\n      ])\n    })\n    it('should fail and release when quorum become dead', async function () {\n      this.timeout(60000)\n      const onLockLostCallbacks = [1, 2, 3].map(() =>\n        sinon.spy(function (this: RedlockSemaphore) {\n          expect(this.isAcquired).to.be.false\n        })\n      )\n      const semaphores1 = [1, 2, 3].map(\n        (n, i) =>\n          new RedlockSemaphore(allClients, 'key', 3, {\n            ...timeoutOptions,\n            onLockLost: onLockLostCallbacks[i]\n          })\n      )\n      await Promise.all(semaphores1.map(s => s.acquire()))\n\n      expect(await client1.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphores1[0].identifier,\n        semaphores1[1].identifier,\n        semaphores1[2].identifier\n      ])\n      expect(await client2.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphores1[0].identifier,\n        semaphores1[1].identifier,\n        semaphores1[2].identifier\n      ])\n      expect(await client3.zrange('semaphore:key', 0, -1)).to.have.members([\n        semaphores1[0].identifier,\n        semaphores1[1].identifier,\n        semaphores1[2].identifier\n      ])\n\n      await downRedisServer(1)\n      console.log('SHUT DOWN 1')\n\n      await downRedisServer(2)\n      console.log('SHUT DOWN 2')\n\n      await delay(1000)\n\n      for (const lostCb of onLockLostCallbacks) {\n        expect(lostCb).to.be.called\n        expect(lostCb.firstCall.firstArg instanceof LostLockError).to.be.true\n      }\n\n      // released lock on server3\n      expect(await client3.zrange('semaphore:key', 0, -1)).to.be.eql([])\n\n      // semaphore2 will NOT be able to acquire the lock\n\n      const semaphore2 = new RedlockSemaphore(\n        allClients,\n        'key',\n        3,\n        timeoutOptions\n      )\n      await expect(semaphore2.acquire()).to.be.rejectedWith(\n        'Acquire redlock-semaphore semaphore:key timeout'\n      )\n    })\n  })\n  describe('ioredis-mock support', () => {\n    it('should acquire and release semaphore', async () => {\n      const semaphore1 = new RedlockSemaphore(allClientMocks, 'key', 2)\n      const semaphore2 = new RedlockSemaphore(allClientMocks, 'key', 2)\n      await semaphore1.acquire()\n      await semaphore2.acquire()\n      await semaphore1.release()\n      await semaphore2.release()\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/index.test.ts",
    "content": "import { expect } from 'chai'\nimport {\n  defaultTimeoutOptions,\n  MultiSemaphore,\n  Mutex,\n  RedlockMultiSemaphore,\n  RedlockMutex,\n  RedlockSemaphore,\n  Semaphore\n} from '../../src/index'\n\ndescribe('index', () => {\n  it('should export public API', () => {\n    expect(Mutex).to.be.ok\n    expect(Semaphore).to.be.ok\n    expect(MultiSemaphore).to.be.ok\n    expect(RedlockMutex).to.be.ok\n    expect(RedlockSemaphore).to.be.ok\n    expect(RedlockMultiSemaphore).to.be.ok\n    expect(defaultTimeoutOptions).to.be.ok\n  })\n})\n"
  },
  {
    "path": "test/src/multiSemaphore/acquire/index.test.ts",
    "content": "import { expect } from 'chai'\n\nimport {\n  acquireSemaphore as acquire,\n  Options\n} from '../../../../src/multiSemaphore/acquire/index'\nimport { client1 as client } from '../../../redisClient'\n\nconst opts = (id: string, overrides?: Partial<Options>): Options => ({\n  identifier: id,\n  acquireTimeout: 50,\n  acquireAttemptsLimit: Number.POSITIVE_INFINITY,\n  lockTimeout: 100,\n  retryInterval: 10,\n  ...overrides\n})\n\ndescribe('multiSemaphore acquire', () => {\n  it('should return true for success acquire', async () => {\n    const result = await acquire(client, 'key', 1, 1, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])\n    expect(result).to.be.true\n  })\n  it('should return false when timeout', async () => {\n    const result1 = await acquire(client, 'key', 2, 1, opts('111')) // expire after 100ms\n    const result2 = await acquire(client, 'key', 2, 1, opts('112')) // expire after 100ms\n    const result3 = await acquire(client, 'key', 2, 1, opts('113')) // timeout after 50ms\n\n    expect(result1).to.be.true\n    expect(result2).to.be.true\n    expect(result3).to.be.false\n  })\n  it('should return false after acquireAttemptsLimit', async () => {\n    const result1 = await acquire(client, 'key', 2, 1, opts('111')) // expire after 100ms\n    const result2 = await acquire(client, 'key', 2, 1, opts('112')) // expire after 100ms\n    const result3 = await acquire(\n      client,\n      'key',\n      2,\n      1,\n      opts('113', {\n        acquireAttemptsLimit: 1,\n        acquireTimeout: Number.POSITIVE_INFINITY\n      })\n    ) // no timeout, attempt limit = 1\n\n    expect(result1).to.be.true\n    expect(result2).to.be.true\n    expect(result3).to.be.false\n  })\n})\n"
  },
  {
    "path": "test/src/multiSemaphore/acquire/internal.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { acquireLua } from '../../../../src/multiSemaphore/acquire/lua'\nimport { client1 as client } from '../../../redisClient'\n\ninterface Options {\n  identifier: string\n  lockTimeout: number\n  now: number\n}\n\nconst opts = (id: string, nowOffset = 0): Options => ({\n  identifier: id,\n  lockTimeout: 500,\n  now: new Date().getTime() + nowOffset\n})\n\nasync function acquire(options: Options) {\n  const { identifier, lockTimeout, now } = options\n  return await acquireLua(client, ['key', 1, 1, identifier, lockTimeout, now])\n}\n\ndescribe('multiSemaphore acquire internal', () => {\n  it('should return 1 for success acquire', async () => {\n    const result = await acquire(opts('111'))\n    expect(result).to.be.eql(1)\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])\n  })\n  it('should return 0 for failure acquire', async () => {\n    const result1 = await acquire(opts('111'))\n    const result2 = await acquire(opts('112'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])\n    expect(result1).to.be.eql(1)\n    expect(result2).to.be.eql(0)\n  })\n  describe('TIME SHIFT case', () => {\n    it('should handle time difference less than lockTimeout (nodeA has faster clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeA is for 450ms faster than nodeB\n      const resultA = await acquire(opts('111', 450))\n      const resultB = await acquire(opts('112', 0))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(0)\n    })\n    it('should handle time difference less than lockTimeout (nodeA has slower clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeB is for 450ms faster than nodeA\n      const resultA = await acquire(opts('111', 0))\n      const resultB = await acquire(opts('112', 450))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(0)\n    })\n    it('cant handle time difference greater than lockTimeout (nodeA has slower clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeB is for 550ms faster than nodeA\n      const resultA = await acquire(opts('111', 0))\n      const resultB = await acquire(opts('112', 550))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(1) // Semaphore stealed...\n\n      // This happens due removing \"expired\" nodeA lock (at nodeB \"now\" nodeA lock has been expired 50ms ago)\n      // Unfortunatelly \"fair\" semaphore described here\n      // https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/\n      // also has the same problem\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/multiSemaphore/refresh/index.test.ts",
    "content": "import { expect } from 'chai'\n\nimport {\n  Options,\n  refreshSemaphore as refresh\n} from '../../../../src/multiSemaphore/refresh/index'\nimport { client1 as client } from '../../../redisClient'\n\nconst opts = (id: string): Options => ({\n  identifier: id,\n  lockTimeout: 100\n})\n\ndescribe('multiSemaphore refresh', () => {\n  it('should return false if resource is already acquired', async () => {\n    const now = '' + (Date.now() - 10)\n    await client.zadd('key', now, '222', now, '333', now, '444')\n    const result = await refresh(client, 'key', 3, 2, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])\n    expect(result).to.be.false\n  })\n  it('should return false if resource is already acquired, but some expired', async () => {\n    const now = '' + (Date.now() - 10)\n    const oldNow = '' + (Date.now() - 10000)\n    await client.zadd('key', oldNow, '222', oldNow, '333', now, '444')\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])\n    const result = await refresh(client, 'key', 3, 2, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['444'])\n    expect(result).to.be.false\n  })\n  it('should return false if resource is not acquired', async () => {\n    const result = await refresh(client, 'key', 3, 2, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql([])\n    expect(result).to.be.false\n  })\n  it('should return true for success refresh', async () => {\n    const now = '' + (Date.now() - 10)\n    await client.zadd('key', now, '111_0', now, '111_1', now, '333')\n    expect(await client.zrange('key', 0, -1)).to.be.eql([\n      '111_0',\n      '111_1',\n      '333'\n    ])\n    const result = await refresh(client, 'key', 3, 2, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql([\n      '333',\n      '111_0',\n      '111_1'\n    ])\n    expect(result).to.be.true\n  })\n})\n"
  },
  {
    "path": "test/src/multiSemaphore/release/index.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { releaseSemaphore as release } from '../../../../src/multiSemaphore/release/index'\nimport { client1 as client } from '../../../redisClient'\n\ndescribe('multiSemaphore release', () => {\n  it('should remove key after success release', async () => {\n    await client.zadd('key', '' + Date.now(), '111_0')\n    expect(await client.zcard('key')).to.be.eql(1)\n    await release(client, 'key', 1, '111')\n    expect(await client.zcard('key')).to.be.eql(0)\n  })\n  it('should do nothing if resource is not locked', async () => {\n    expect(await client.zcard('key')).to.be.eql(0)\n    await release(client, 'key', 1, '111')\n    expect(await client.zcard('key')).to.be.eql(0)\n  })\n})\n"
  },
  {
    "path": "test/src/mutex/acquire.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { acquireMutex as acquire, Options } from '../../../src/mutex/acquire'\nimport { client1 as client } from '../../redisClient'\n\nconst opts = (id: string, overrides?: Partial<Options>): Options => ({\n  identifier: id,\n  acquireTimeout: 50,\n  acquireAttemptsLimit: Number.POSITIVE_INFINITY,\n  lockTimeout: 100,\n  retryInterval: 10,\n  ...overrides\n})\n\ndescribe('mutex acquire', () => {\n  it('should return true for success lock', async () => {\n    const result = await acquire(client, 'key', opts('111'))\n    expect(result).to.be.true\n  })\n  it('should return false when timeout', async () => {\n    const result1 = await acquire(client, 'key', opts('111'))\n    const result2 = await acquire(client, 'key', opts('222'))\n    expect(result1).to.be.true\n    expect(result2).to.be.false\n  })\n  it('should return false after acquireAttemptsLimit', async () => {\n    const result1 = await acquire(client, 'key', opts('111'))\n    const result2 = await acquire(\n      client,\n      'key',\n      opts('222', {\n        acquireAttemptsLimit: 1,\n        acquireTimeout: Number.POSITIVE_INFINITY\n      })\n    )\n    expect(result1).to.be.true\n    expect(result2).to.be.false\n  })\n  it('should set identifier for key', async () => {\n    await acquire(client, 'key1', opts('111'))\n    const value = await client.get('key1')\n    expect(value).to.be.eql('111')\n  })\n  it('should set TTL for key', async () => {\n    await acquire(client, 'key2', opts('111'))\n    const ttl = await client.pttl('key2')\n    expect(ttl).to.be.gte(90)\n    expect(ttl).to.be.lte(100)\n  })\n  it('should wait for auto-release', async () => {\n    const start1 = Date.now()\n    await acquire(client, 'key', opts('111'))\n    const start2 = Date.now()\n    await acquire(client, 'key', opts('222'))\n    const now = Date.now()\n    expect(start2 - start1).to.be.gte(0)\n    expect(start2 - start1).to.be.lt(10)\n    expect(now - start1).to.be.gte(50)\n    expect(now - start2).to.be.gte(50)\n  })\n  it('should wait per key', async () => {\n    const start1 = Date.now()\n    await Promise.all([\n      acquire(client, 'key1', opts('a1')),\n      acquire(client, 'key2', opts('a2'))\n    ])\n    const start2 = Date.now()\n    await Promise.all([\n      acquire(client, 'key1', opts('b1')),\n      acquire(client, 'key2', opts('b2'))\n    ])\n    const now = Date.now()\n    expect(start2 - start1).to.be.gte(0)\n    expect(start2 - start1).to.be.lt(10)\n    expect(now - start1).to.be.gte(50)\n    expect(now - start2).to.be.gte(50)\n  })\n})\n"
  },
  {
    "path": "test/src/mutex/refresh.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { refreshMutex as refresh } from '../../../src/mutex/refresh'\nimport { client1 as client } from '../../redisClient'\n\ndescribe('mutex refresh', () => {\n  it('should return false if resource is already acquired by different instance', async () => {\n    await client.set('key', '222')\n    const result = await refresh(client, 'key', '111', 10000)\n    expect(result).to.be.false\n  })\n  it('should return false if resource is not acquired', async () => {\n    const result = await refresh(client, 'key', '111', 10000)\n    expect(result).to.be.false\n  })\n  it('should return true for success refresh', async () => {\n    await client.set('key', '111')\n    const result = await refresh(client, 'key', '111', 20000)\n    expect(result).to.be.true\n    expect(await client.pttl('key')).to.be.gte(10000)\n  })\n})\n"
  },
  {
    "path": "test/src/mutex/release.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { releaseMutex as release } from '../../../src/mutex/release'\nimport { client1 as client } from '../../redisClient'\n\ndescribe('Mutex release', () => {\n  it('should remove key after release', async () => {\n    await client.set('key', '111')\n    await release(client, 'key', '111')\n    expect(await client.get('key')).to.be.eql(null)\n  })\n  it('should do nothing if resource is not locked', async () => {\n    expect(await client.get('key')).to.be.eql(null)\n    await release(client, 'key', '111')\n    expect(await client.get('key')).to.be.eql(null)\n  })\n})\n"
  },
  {
    "path": "test/src/redlockMutex/acquire.test.ts",
    "content": "import { expect } from 'chai'\n\nimport {\n  acquireRedlockMutex as acquire,\n  Options\n} from '../../../src/redlockMutex/acquire'\nimport { allClients } from '../../redisClient'\n\nconst opts = (id: string, overrides?: Partial<Options>): Options => ({\n  identifier: id,\n  acquireTimeout: 50,\n  acquireAttemptsLimit: Number.POSITIVE_INFINITY,\n  lockTimeout: 100,\n  retryInterval: 10,\n  ...overrides\n})\n\ndescribe('redlockMutex acquire', () => {\n  it('should return true for success lock', async () => {\n    const result = await acquire(allClients, 'key', opts('111'))\n    expect(result).to.be.true\n  })\n  it('should return false when timeout', async () => {\n    const result1 = await acquire(allClients, 'key', opts('111'))\n    const result2 = await acquire(allClients, 'key', opts('222'))\n    expect(result1).to.be.true\n    expect(result2).to.be.false\n  })\n  it('should return false after acquireAttemptsLimit', async () => {\n    const result1 = await acquire(allClients, 'key', opts('111'))\n    const result2 = await acquire(\n      allClients,\n      'key',\n      opts('222', {\n        acquireAttemptsLimit: 1,\n        acquireTimeout: Number.POSITIVE_INFINITY\n      })\n    )\n    expect(result1).to.be.true\n    expect(result2).to.be.false\n  })\n  it('should set identifier for key', async () => {\n    await acquire(allClients, 'key1', opts('111'))\n    const values = await Promise.all(\n      allClients.map(client => client.get('key1'))\n    )\n    expect(values).to.be.eql(['111', '111', '111'])\n  })\n  it('should set TTL for key', async () => {\n    await acquire(allClients, 'key2', opts('111'))\n    const ttls = await Promise.all(\n      allClients.map(client => client.pttl('key2'))\n    )\n    for (const ttl of ttls) {\n      if (ttl === -2) {\n        continue\n      }\n      expect(ttl).to.be.gte(90)\n      expect(ttl).to.be.lte(100)\n    }\n  })\n  it('should wait for auto-release', async () => {\n    const start1 = Date.now()\n    await acquire(allClients, 'key', opts('111'))\n    const start2 = Date.now()\n    await acquire(allClients, 'key', opts('222'))\n    const now = Date.now()\n    expect(start2 - start1).to.be.gte(0)\n    expect(start2 - start1).to.be.lt(10)\n    expect(now - start1).to.be.gte(50)\n    expect(now - start2).to.be.gte(50)\n  })\n  it('should wait per key', async () => {\n    const start1 = Date.now()\n    await Promise.all([\n      acquire(allClients, 'key1', opts('a1')),\n      acquire(allClients, 'key2', opts('a2'))\n    ])\n    const start2 = Date.now()\n    await Promise.all([\n      acquire(allClients, 'key1', opts('b1')),\n      acquire(allClients, 'key2', opts('b2'))\n    ])\n    const now = Date.now()\n    expect(start2 - start1).to.be.gte(0)\n    expect(start2 - start1).to.be.lt(10)\n    expect(now - start1).to.be.gte(50)\n    expect(now - start2).to.be.gte(50)\n  })\n})\n"
  },
  {
    "path": "test/src/redlockMutex/refresh.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { refreshRedlockMutex as refresh } from '../../../src/redlockMutex/refresh'\nimport { allClients, client1, client2, client3 } from '../../redisClient'\n\ndescribe('redlockMutex refresh', () => {\n  it('should return false if resource is acquired by different instance on quorum', async () => {\n    await client1.set('key', '111')\n    await client2.set('key', '222')\n    await client3.set('key', '222')\n    const result = await refresh(allClients, 'key', '111', 10000)\n    expect(result).to.be.false\n  })\n  it('should return true if resource is acquired on quorum', async () => {\n    await client1.set('key', '111')\n    await client2.set('key', '111')\n    const result = await refresh(allClients, 'key', '111', 20000)\n    expect(result).to.be.true\n    expect(await client1.pttl('key')).to.be.gte(10000)\n    expect(await client2.pttl('key')).to.be.gte(10000)\n  })\n  it('should return false if resource is not acquired on quorum', async () => {\n    await client1.set('key', '111')\n    const result = await refresh(allClients, 'key', '111', 10000)\n    expect(result).to.be.false\n  })\n})\n"
  },
  {
    "path": "test/src/redlockMutex/release.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { releaseRedlockMutex as release } from '../../../src/redlockMutex/release'\nimport { allClients, client1 } from '../../redisClient'\n\ndescribe('redlockMutex release', () => {\n  it('should remove key after release', async () => {\n    await client1.set('key', '111')\n    await release(allClients, 'key', '111')\n    expect(await client1.get('key')).to.be.eql(null)\n  })\n  it('should do nothing if resource is not locked', async () => {\n    expect(await client1.get('key')).to.be.eql(null)\n    await release(allClients, 'key', '111')\n    expect(await client1.get('key')).to.be.eql(null)\n  })\n})\n"
  },
  {
    "path": "test/src/semaphore/acquire/index.test.ts",
    "content": "import { expect } from 'chai'\n\nimport {\n  acquireSemaphore as acquire,\n  Options\n} from '../../../../src/semaphore/acquire/index'\nimport { client1 as client } from '../../../redisClient'\n\nconst opts = (id: string, overrides?: Partial<Options>): Options => ({\n  identifier: id,\n  acquireTimeout: 50,\n  acquireAttemptsLimit: Number.POSITIVE_INFINITY,\n  lockTimeout: 100,\n  retryInterval: 10,\n  ...overrides\n})\n\ndescribe('semaphore acquire', () => {\n  it('should return true for success acquire', async () => {\n    const result = await acquire(client, 'key', 1, opts('111'))\n    expect(result).to.be.true\n  })\n  it('should return false when timeout', async () => {\n    const result1 = await acquire(client, 'key', 2, opts('111')) // expire after 100ms\n    const result2 = await acquire(client, 'key', 2, opts('112')) // expire after 100ms\n    const result3 = await acquire(client, 'key', 2, opts('113')) // timeout after 50ms\n\n    expect(result1).to.be.true\n    expect(result2).to.be.true\n    expect(result3).to.be.false\n  })\n  it('should return false after acquireAttemptsLimit', async () => {\n    const result1 = await acquire(client, 'key', 2, opts('111')) // expire after 100ms\n    const result2 = await acquire(client, 'key', 2, opts('112')) // expire after 100ms\n    const result3 = await acquire(\n      client,\n      'key',\n      2,\n      opts('113', {\n        acquireAttemptsLimit: 1,\n        acquireTimeout: Number.POSITIVE_INFINITY\n      })\n    ) // no timeout, acquire limit = 1\n\n    expect(result1).to.be.true\n    expect(result2).to.be.true\n    expect(result3).to.be.false\n  })\n})\n"
  },
  {
    "path": "test/src/semaphore/acquire/internal.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { acquireLua } from '../../../../src/semaphore/acquire/lua'\nimport { client1 as client } from '../../../redisClient'\n\ninterface Options {\n  identifier: string\n  lockTimeout: number\n  now: number\n}\n\nconst opts = (id: string, nowOffset = 0): Options => ({\n  identifier: id,\n  lockTimeout: 500,\n  now: new Date().getTime() + nowOffset\n})\n\nasync function acquire(options: Options) {\n  const { identifier, lockTimeout, now } = options\n  return await acquireLua(client, ['key', 1, identifier, lockTimeout, now])\n}\n\ndescribe('semaphore acquire internal', () => {\n  it('should return 1 for success acquire', async () => {\n    const result = await acquire(opts('111'))\n    expect(result).to.be.eql(1)\n  })\n  it('should return 0 for failure acquire', async () => {\n    const result1 = await acquire(opts('111'))\n    const result2 = await acquire(opts('112'))\n    expect(result1).to.be.eql(1)\n    expect(result2).to.be.eql(0)\n  })\n  describe('TIME SHIFT case', () => {\n    it('should handle time difference less than lockTimeout (nodeA has faster clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeA is for 450ms faster than nodeB\n      const resultA = await acquire(opts('111', 450))\n      const resultB = await acquire(opts('112', 0))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(0)\n    })\n    it('should handle time difference less than lockTimeout (nodeA has slower clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeB is for 450ms faster than nodeA\n      const resultA = await acquire(opts('111', 0))\n      const resultB = await acquire(opts('112', 450))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(0)\n    })\n    it('cant handle time difference greater than lockTimeout (nodeA has slower clocks)', async () => {\n      // lockTimeout = 500ms\n      // nodeB is for 550ms faster than nodeA\n      const resultA = await acquire(opts('111', 0))\n      const resultB = await acquire(opts('112', 550))\n      expect(resultA).to.be.eql(1)\n      expect(resultB).to.be.eql(1) // Semaphore stealed...\n\n      // This happens due removing \"expired\" nodeA lock (at nodeB \"now\" nodeA lock has been expired 50ms ago)\n      // Unfortunatelly \"fair\" semaphore described here\n      // https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/\n      // also has the same problem\n    })\n  })\n})\n"
  },
  {
    "path": "test/src/semaphore/refresh/index.test.ts",
    "content": "import { expect } from 'chai'\n\nimport {\n  Options,\n  refreshSemaphore as refresh\n} from '../../../../src/semaphore/refresh/index'\nimport { client1 as client } from '../../../redisClient'\n\nconst opts = (id: string): Options => ({\n  identifier: id,\n  lockTimeout: 100\n})\n\ndescribe('semaphore refresh', () => {\n  it('should return false if resource is already acquired', async () => {\n    const now = '' + (Date.now() - 10)\n    await client.zadd('key', now, '222', now, '333', now, '444')\n    const result = await refresh(client, 'key', 3, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])\n    expect(result).to.be.false\n  })\n  it('should return false if resource is already acquired, but some expired', async () => {\n    const now = '' + (Date.now() - 10)\n    const oldNow = '' + (Date.now() - 10000)\n    await client.zadd('key', oldNow, '222', now, '333', now, '444')\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])\n    const result = await refresh(client, 'key', 3, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['333', '444'])\n    expect(result).to.be.false\n  })\n  it('should return false if resource is not acquired', async () => {\n    const result = await refresh(client, 'key', 3, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql([])\n    expect(result).to.be.false\n  })\n  it('should return true for success refresh', async () => {\n    const now = '' + (Date.now() - 10)\n    await client.zadd('key', now, '111', now, '222', now, '333')\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['111', '222', '333'])\n    const result = await refresh(client, 'key', 3, opts('111'))\n    expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '111'])\n    expect(result).to.be.true\n  })\n})\n"
  },
  {
    "path": "test/src/semaphore/release.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { releaseSemaphore as release } from '../../../src/semaphore/release'\nimport { client1 as client } from '../../redisClient'\n\ndescribe('semaphore release', () => {\n  it('should remove key after success release', async () => {\n    await client.zadd('key', '' + Date.now(), '111')\n    expect(await client.zcard('key')).to.be.eql(1)\n    await release(client, 'key', '111')\n    expect(await client.zcard('key')).to.be.eql(0)\n  })\n  it('should do nothing if resource is not locked', async () => {\n    expect(await client.zcard('key')).to.be.eql(0)\n    await release(client, 'key', '111')\n    expect(await client.zcard('key')).to.be.eql(0)\n  })\n})\n"
  },
  {
    "path": "test/src/utils/eval.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { createEval } from '../../../src/utils/index'\nimport { client1 as client } from '../../redisClient'\n\ndescribe('utils createEval', () => {\n  it('should return function', async () => {\n    expect(createEval('return 5', 0)).to.be.a('function')\n  })\n  it('should call evalsha or fallback to eval', async () => {\n    const now = Date.now()\n    const SCRIPT = `return ${now}`\n    const execScript = createEval(SCRIPT, 0)\n    const result = await execScript(client, [])\n    expect(result).to.be.eql(now)\n    expect(Date.now() - now).to.be.lt(50)\n  })\n  it('should handle eval errors', async () => {\n    const execScript = createEval('return asdfkasjdf', 0)\n    await expect(execScript(client, [])).to.be.rejected\n  })\n})\n"
  },
  {
    "path": "test/src/utils/index.test.ts",
    "content": "import { expect } from 'chai'\nimport Redis from 'ioredis'\nimport { getConnectionName } from '../../../src/utils/index'\nimport { client1 } from '../../redisClient'\n\ndescribe('utils getConnectionName', () => {\n  it('should return connection name', async () => {\n    expect(getConnectionName(client1)).to.be.eql('<client1>')\n  })\n  it('should return unknown if connection name not configured', () => {\n    const client = new Redis('redis://127.0.0.1:6000', {\n      lazyConnect: true,\n      enableOfflineQueue: false,\n      autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)\n      maxRetriesPerRequest: 0 // dont retry, fail faster (default is 20)\n    })\n    expect(getConnectionName(client)).to.be.eql('<unknown client>')\n  })\n})\n"
  },
  {
    "path": "test/src/utils/redlock.test.ts",
    "content": "import { expect } from 'chai'\n\nimport { getQuorum } from '../../../src/utils/redlock'\n\ndescribe('redlockMutex utils', () => {\n  describe('getQuorum', () => {\n    function makeTest(count: number, expectedResult: number) {\n      it(`should return valid majority for ${count} nodes`, () => {\n        expect(getQuorum(count)).to.be.eql(expectedResult)\n        expect(getQuorum(2)).to.be.eql(2)\n        expect(getQuorum(3)).to.be.eql(2)\n        expect(getQuorum(4)).to.be.eql(3)\n        expect(getQuorum(5)).to.be.eql(3)\n        expect(getQuorum(6)).to.be.eql(4)\n        expect(getQuorum(7)).to.be.eql(4)\n        expect(getQuorum(8)).to.be.eql(5)\n        expect(getQuorum(9)).to.be.eql(5)\n      })\n    }\n    // makeTest(0, 1)\n    makeTest(1, 1)\n    makeTest(2, 2)\n    makeTest(3, 2)\n    makeTest(4, 3)\n    makeTest(5, 3)\n    makeTest(6, 4)\n    makeTest(7, 4)\n    makeTest(8, 5)\n    makeTest(9, 5)\n  })\n})\n"
  },
  {
    "path": "test/unhandledRejection.ts",
    "content": "import sinon from 'sinon'\n\nfunction throwReason(reason: any) {\n  console.log('unhandled rejection:', reason)\n  throw reason\n}\n\nexport const unhandledRejectionSpy = sinon.spy()\n\nexport function catchUnhandledRejection() {\n  unhandledRejectionSpy.resetHistory()\n  process.removeListener('unhandledRejection', throwReason)\n  process.on('unhandledRejection', unhandledRejectionSpy)\n}\n\nexport function throwUnhandledRejection() {\n  process.removeListener('unhandledRejection', unhandledRejectionSpy)\n  process.on('unhandledRejection', throwReason)\n}\n\nexport function init() {\n  process.on('unhandledRejection', throwReason)\n}\n\nexport function removeAllListeners() {\n  process.removeListener('unhandledRejection', unhandledRejectionSpy)\n  process.removeListener('unhandledRejection', throwReason)\n}\n"
  },
  {
    "path": "tsconfig.build-commonjs.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"*.test.ts\"],\n  \"compilerOptions\": {\n    \"module\": \"CommonJS\",\n    \"outDir\": \"lib\"\n  }\n}\n"
  },
  {
    "path": "tsconfig.build-es.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"*.test.ts\"],\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"outDir\": \"es\"\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"ES6\"],\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"Node\",\n    \"sourceMap\": true,\n    \"strict\": true\n  }\n}\n"
  }
]