{"_id":"@codylabs/redlock","_rev":"3-2151f1c5fca7df4d034a6b1b3f94d801","name":"@codylabs/redlock","dist-tags":{"latest":"0.1.0"},"versions":{"0.0.1":{"name":"@codylabs/redlock","version":"0.0.1","author":"Cody Nguyen","license":"MIT","_id":"@codylabs/redlock@0.0.1","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"dist":{"shasum":"aa8d07ac37081fde47cb39cab38315ccb78a380d","tarball":"https://registry.npmjs.org/@codylabs/redlock/-/redlock-0.0.1.tgz","fileCount":6,"integrity":"sha512-ymumxi8wINmTq8xLooNICEkNKkTUL903FvzAeGMMzz2AyPmY22ZFlgElL6Ei0fQ/9UMXGSHpeDDwe9T4zHWnPg==","signatures":[{"sig":"MEUCIFGwvAIVpOp19t+L/L7g5I65fcq6ayPyph73aVIuxuJFAiEAmBxjyRCkviT8EAvWPd1vVtugdLur8K6zUnKYQHvy2Vs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":56808},"main":"./dist/index.cjs","types":"./dist/index.d.cts","module":"./dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"scripts":{"test":"vitest run","build":"tsdown src/index.ts --format cjs,esm --dts","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test:watch":"vitest"},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"description":"","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsdown":"^0.22.0","vitest":"^4.1.7","typescript":"^6.0.3","@codylabs/typescript-configs":"0.0.8"},"peerDependencies":{"redis":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/redlock_0.0.1_1779866804450_0.24119159608047158","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@codylabs/redlock","version":"0.0.2","author":"Cody Nguyen","license":"MIT","_id":"@codylabs/redlock@0.0.2","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"homepage":"https://github.com/cuongnd1705/codylabs#readme","bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"dist":{"shasum":"a2780e517fc5a3037e62c4890f0f15a58c8da8a4","tarball":"https://registry.npmjs.org/@codylabs/redlock/-/redlock-0.0.2.tgz","fileCount":6,"integrity":"sha512-8ArZxyEaN6ufCb64DRRghJTXinz50kdrR/Mk9KqrmiHcvcwe4FeQaoxnMjKDsgJoHyXThavMau/heEpEAZlDLw==","signatures":[{"sig":"MEQCIDfHXia9s9K3lbd6oxhHD3exScJgyNyjFb71vhB8C8ESAiAVh/NiVWQxDpSM9mpFwiiglF/yoY4sJD82rxudqgFQlA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57100},"main":"./dist/index.cjs","types":"./dist/index.d.cts","module":"./dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"scripts":{"test":"vitest run","build":"tsdown src/index.ts --format cjs,esm --dts","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test:watch":"vitest"},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"repository":{"url":"git+https://github.com/cuongnd1705/codylabs.git","type":"git"},"description":"Package @codylabs/redlock from Codylabs monorepo","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsdown":"^0.22.1","vitest":"^4.1.7","typescript":"^6.0.3","@codylabs/typescript-configs":"0.0.9"},"peerDependencies":{"redis":"^6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/redlock_0.0.2_1780126504112_0.03360240851954255","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@codylabs/redlock","version":"0.1.0","description":"Package @codylabs/redlock from Codylabs monorepo","homepage":"https://github.com/cuongnd1705/codylabs#readme","bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"license":"MIT","author":"Cody Nguyen","repository":{"type":"git","url":"git+https://github.com/cuongnd1705/codylabs.git"},"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.cts","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"publishConfig":{"access":"public"},"devDependencies":{"@codylabs/typescript-configs":"0.1.0","tsdown":"^0.22.14","typescript":"^6.0.3","vitest":"^4.1.11"},"peerDependencies":{"redis":"^6.0.0"},"scripts":{"build":"tsdown src/index.ts --format cjs,esm --dts","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test":"vitest run","test:watch":"vitest"},"_nodeVersion":"24.20.0","_id":"@codylabs/redlock@0.1.0","dist":{"integrity":"sha512-McEorPzk4wN8rK52B5BQXK+pbmKZiMNGZOCbX3OZlwJO4HUom/BYS+ez7nNCYu0qAykKRuTlKBfUl8HSxftOTQ==","shasum":"f9ebda250146048140c52eb054b51bbac60be56b","tarball":"https://registry.npmjs.org/@codylabs/redlock/-/redlock-0.1.0.tgz","fileCount":6,"unpackedSize":57168,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC7h6BTxcrhcNq1vUjeTxNdZBrF/xVJpWlVG9yelviCtgIhAIaZTuhw6PF5EqTTGEjyac/k1wDPVKs8N9qxqFDnaWrz"}]},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"directories":{},"maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/redlock_0.1.0_1788017130895_0.391603617414811"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-27T07:26:44.215Z","modified":"2026-08-29T15:25:31.211Z","0.0.1":"2026-05-27T07:26:44.626Z","0.0.2":"2026-05-30T07:35:04.242Z","0.1.0":"2026-08-29T15:25:31.042Z"},"bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"author":"Cody Nguyen","license":"MIT","homepage":"https://github.com/cuongnd1705/codylabs#readme","repository":{"type":"git","url":"git+https://github.com/cuongnd1705/codylabs.git"},"description":"Package @codylabs/redlock from Codylabs monorepo","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"readme":"# @codylabs/redlock\n\nA TypeScript implementation of the [Redlock algorithm](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/) for distributed locking with Redis. Provides mutual exclusion, deadlock freedom, and fault tolerance across multiple independent Redis instances.\n\n## Features\n\n- Redlock algorithm with majority quorum consensus\n- **Early quorum resolution** — resolves as soon as N/2+1 nodes agree, without waiting for slow/failing nodes\n- **EVALSHA caching** — sends Lua script hash instead of full script after first use; falls back to EVAL automatically\n- Multi-resource (multi-key) atomic locking with deadlock prevention via sorted key ordering\n- `withLock` helper with **AbortSignal** support — routine can detect when auto-extension fails and abort safely\n- Per-call option overrides for `acquire` and `withLock` (retry delay, jitter, max attempts)\n- Unlimited retries (`maxRetryAttempts: -1`)\n- Automatic lock extension\n- Cryptographically secure token generation\n- Retry with configurable delay and symmetric random jitter (avoids thundering herd)\n- Clock drift compensation per the Redlock spec\n- Atomic acquire / release / extend via Lua scripts\n- `quit()` for graceful connection teardown\n\n## Installation\n\n```sh\n# npm\nnpm install @codylabs/redlock redis\n\n# pnpm\npnpm add @codylabs/redlock redis\n```\n\n`redis` (v5+) is a required peer dependency.\n\n## Usage\n\n### Setup\n\nThe Redlock algorithm requires **N independent Redis instances** (not replicas). The recommended setup is 5 instances, giving a quorum of 3.\n\n```typescript\nimport { createClient } from 'redis';\nimport { Redlock } from '@codylabs/redlock';\n\nconst clients = [\n  createClient({ url: 'redis://redis1:6379' }),\n  createClient({ url: 'redis://redis2:6379' }),\n  createClient({ url: 'redis://redis3:6379' }),\n  createClient({ url: 'redis://redis4:6379' }),\n  createClient({ url: 'redis://redis5:6379' }),\n];\n\nawait Promise.all(clients.map((c) => c.connect()));\n\nconst redlock = new Redlock(clients, {\n  driftFactor: 0.01, // clock drift compensation (1% of TTL + 2ms constant)\n  retryDelayMs: 200, // base retry delay\n  retryJitterMs: 100, // symmetric jitter (±100ms) to avoid thundering herd\n  maxRetryAttempts: 3, // max acquisition attempts; use -1 for unlimited\n});\n```\n\n### Basic: `acquire` / `release`\n\n```typescript\nconst lock = await redlock.acquire('my-resource', 30_000); // TTL: 30s\n\nif (!lock) {\n  throw new Error('Could not acquire lock');\n}\n\ntry {\n  // critical section\n} finally {\n  await lock.release();\n}\n```\n\n### Recommended: `withLock`\n\nAutomatically acquires, optionally extends, and always releases the lock. The routine receives an `AbortSignal` that is aborted if auto-extension fails mid-execution:\n\n```typescript\nconst result = await redlock.withLock('my-resource', 30_000, async (signal) => {\n  const data = await fetchData();\n  if (signal.aborted) throw signal.reason; // lock was lost\n  return processData(data);\n});\n```\n\nWith automatic extension (extends the lock before it expires while the function runs):\n\n```typescript\nconst result = await redlock.withLock(\n  'my-resource',\n  30_000,\n  async (signal) => {\n    const data = await fetchData();\n    if (signal.aborted) throw signal.reason;\n    return processData(data);\n  },\n  { extensionThresholdMs: 5_000 }, // extend 5s before expiry\n);\n```\n\n### Per-call option overrides\n\nOverride instance-level defaults for a single `acquire` or `withLock` call:\n\n```typescript\n// Retry aggressively for a high-priority resource\nconst lock = await redlock.acquire('critical-job', 10_000, {\n  retryDelayMs: 50,\n  retryJitterMs: 20,\n  maxRetryAttempts: 10,\n});\n\n// Fail fast for a low-priority resource\nawait redlock.withLock('best-effort', 5_000, async (signal) => doWork(signal), {\n  maxRetryAttempts: 0, // try once, don't retry\n});\n```\n\n### Unlimited retries\n\n```typescript\nconst redlock = new Redlock(clients, {\n  maxRetryAttempts: -1, // keep trying until the lock is acquired\n  retryDelayMs: 200,\n  retryJitterMs: 100,\n});\n```\n\n### Multi-resource locking\n\nAcquire locks on multiple resources atomically. Resources are sorted lexicographically before locking to prevent deadlocks.\n\n```typescript\nconst lock = await redlock.acquire(['user:123', 'order:456'], 10_000);\n\nif (!lock) {\n  throw new Error('Could not acquire locks');\n}\n\ntry {\n  // critical section affecting both resources\n} finally {\n  await lock.release();\n}\n```\n\nOr with `withLock`:\n\n```typescript\nawait redlock.withLock(['user:123', 'order:456'], 10_000, async (signal) => {\n  if (signal.aborted) throw signal.reason;\n  await transferFunds(userId, orderId);\n});\n```\n\n### Manual extension\n\n```typescript\nconst lock = await redlock.acquire('my-resource', 5_000);\n\n// ... later, extend by another 5s\nconst extended = await lock.extend(5_000);\nif (!extended) {\n  // lock was lost; abort the operation\n}\n```\n\n### Auto-extension\n\n`startAutoExtension` accepts an optional `onFailure` callback. If omitted a warning is logged; if provided, the callback is called with the error instead (useful for aborting work on lock loss):\n\n```typescript\nconst lock = await redlock.acquire('my-resource', 30_000);\nlock.startAutoExtension(5_000, (err) => {\n  console.error('Lock lost!', err);\n  // signal your work to stop\n});\n\ntry {\n  await longRunningWork();\n} finally {\n  lock.stopAutoExtension();\n  await lock.release();\n}\n```\n\n### Inspecting lock state\n\n```typescript\nlock.isValid; // true if not released and not expired\nlock.isReleased; // true after release() is called\nlock.isExpired; // true if past the TTL\nlock.expirationTime; // Date when the lock expires\nlock.resourceKeys; // string[] of locked keys\n```\n\n### Lifecycle / cleanup\n\n```typescript\n// Gracefully close all Redis connections managed by Redlock\nawait redlock.quit();\n```\n\n### Using Redis Cluster or Sentinel\n\n`Redlock` accepts `RedisClientType`, `RedisClusterType`, and `RedisSentinelType` from the `redis` package.\n\n**Redis Cluster** — the cluster handles replication internally, so a single cluster client counts as one Redlock node:\n\n```typescript\nimport { createCluster } from 'redis';\n\nconst cluster = createCluster({\n  rootNodes: [{ url: 'redis://node1:6379' }, { url: 'redis://node2:6379' }, { url: 'redis://node3:6379' }],\n});\nawait cluster.connect();\n\nconst redlock = new Redlock([cluster]);\n```\n\n**Redis Sentinel** — Sentinel provides HA for a single logical instance, also treated as one Redlock node:\n\n```typescript\nimport { createSentinel } from 'redis';\n\nconst sentinel = createSentinel({\n  sentinelRootNodes: [\n    { host: 'sentinel1', port: 26379 },\n    { host: 'sentinel2', port: 26379 },\n    { host: 'sentinel3', port: 26379 },\n  ],\n  name: 'mymaster',\n});\nawait sentinel.connect();\n\nconst redlock = new Redlock([sentinel]);\n```\n\n> **Note**: For the strongest fault tolerance guarantees of the Redlock algorithm, use multiple independent Redis instances (not replicas of each other). A single Cluster or Sentinel client gives HA for one logical node but does not provide cross-node quorum.\n\n## Configuration\n\n| Option             | Type     | Default | Description                                                                  |\n| ------------------ | -------- | ------- | ---------------------------------------------------------------------------- |\n| `driftFactor`      | `number` | `0.01`  | Clock drift factor (0–0.1). Applied as `driftFactor × TTL + 2ms`.            |\n| `retryDelayMs`     | `number` | `200`   | Base delay in ms between acquisition attempts.                               |\n| `retryJitterMs`    | `number` | `100`   | Symmetric jitter (±N ms) added to retry delay to avoid thundering herd.      |\n| `maxRetryAttempts` | `number` | `3`     | Total acquisition attempts before giving up. Use `-1` for unlimited retries. |\n\nAll options can be overridden per `acquire` / `withLock` call via `AcquireOptions` / `WithLockOptions`.\n\n## Error types\n\n| Class                   | `error.name`              | When thrown                                       |\n| ----------------------- | ------------------------- | ------------------------------------------------- |\n| `InvalidParameterError` | `'InvalidParameterError'` | Invalid arguments (empty key, negative TTL, etc.) |\n| `RedisConnectionError`  | `'RedisConnectionError'`  | Redis operation failure                           |\n\n## Algorithm notes\n\nThis implementation follows the [official Redlock specification](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/):\n\n- **Quorum**: a lock is considered acquired only when `⌊N/2⌋ + 1` instances confirm it.\n- **Early quorum resolution**: operations resolve as soon as the outcome is determined — no waiting for slow or failing nodes.\n- **Effective validity**: `TTL - elapsed - (driftFactor × TTL + 2ms)` — the usable lock lifetime after drift and network latency are subtracted.\n- **Timing**: if the effective validity ≤ 1ms after acquisition, the attempt is rejected even with majority consensus.\n- **EVALSHA caching**: Lua scripts are identified by their SHA1 hash. Redis caches them after the first `EVAL`; subsequent calls use `EVALSHA` (faster, less bandwidth). The library falls back transparently on `NOSCRIPT` errors.\n- **Lua atomicity**: acquire, release, and extend all use atomic Lua scripts to prevent race conditions within each Redis instance.\n- **Multi-resource deadlock prevention**: keys are sorted lexicographically before locking so concurrent callers always acquire in the same order.\n","readmeFilename":""}