diff --git a/CHANGELOG.md b/CHANGELOG.md index 4715c7a..b3fe012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.1.1] - 2026-10-26 +### Fixed +- Disposing a `pettyCache.mutex.lock()` handle no longer rejects when Redis fails. The error is ignored and the lock expires via its TTL, as with the semaphore functions. Before, an `await using` block threw after its work succeeded, or wrapped its own error in a `SuppressedError`. + ## [5.1.0] - 2026-10-26 ### Added - `pettyCache.mutex.lock()` resolves with a lock handle. Declare it with `await using` to release the lock when the block exits, even when it throws. The handle only releases its own lock: if the TTL expired and another caller acquired the lock, disposing the handle leaves the newer lock in place. diff --git a/README.md b/README.md index 8782a09..b507184 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ async function doWork() { } ``` -The handle only releases its own lock. If the TTL expired and another caller acquired the lock, disposing the handle leaves the newer lock in place. Without `await using`, release the lock with `await lock[Symbol.asyncDispose]()`. +The handle only releases its own lock. If the TTL expired and another caller acquired the lock, disposing the handle leaves the newer lock in place. Releasing never rejects: if Redis fails, the error is ignored and the lock expires via its TTL. Without `await using`, release the lock with `await lock[Symbol.asyncDispose]()`. **Options** diff --git a/index.js b/index.js index 57d4e28..4026a6b 100644 --- a/index.js +++ b/index.js @@ -503,8 +503,9 @@ function PettyCache(options) { return { [Symbol.asyncDispose]: async () => { - // Compare-and-delete, so a holder whose TTL expired can't release a newer holder's lock - await redisClient.eval('if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end return 0', { arguments: [value], keys: [key] }); + // Compare-and-delete, so a holder whose TTL expired can't release a newer holder's lock. + // Release errors are ignored; the mutex lock expires via its TTL + await redisClient.eval('if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end return 0', { arguments: [value], keys: [key] }).catch(() => {}); } }; }; diff --git a/package.json b/package.json index cf58f32..512fa54 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,5 @@ "test": "node --test --test-force-exit --test-reporter=spec", "test:only": "node --test --test-force-exit --test-only --test-reporter=spec" }, - "version": "5.1.0" + "version": "5.1.1" } diff --git a/test/index.js b/test/index.js index 712ff6e..07e1aba 100644 --- a/test/index.js +++ b/test/index.js @@ -1834,7 +1834,7 @@ test('PettyCache.mutex.unlock should return error if Redis DEL fails', async () stubClient.del = originalDel; }); -test('PettyCache.mutex.lock handle should return error if Redis EVAL fails', async () => { +test('PettyCache.mutex.lock handle should ignore Redis EVAL errors', async () => { const stubClient = redis.createClient(); const originalEval = stubClient.eval.bind(stubClient); @@ -1843,10 +1843,14 @@ test('PettyCache.mutex.lock handle should return error if Redis EVAL fails', asy const pettyCache = new PettyCache(stubClient); const lock = await pettyCache.mutex.lock(Math.random().toString()); - await assert.rejects( - lock[Symbol.asyncDispose](), - { message: 'Redis EVAL error' } - ); + await assert.doesNotReject(lock[Symbol.asyncDispose]()); + + // The await using block's own error comes through, not wrapped in a SuppressedError + await assert.rejects(async () => { + await using lock = await pettyCache.mutex.lock(Math.random().toString()); + + throw new Error('Work failed'); + }, { message: 'Work failed', name: 'Error' }); stubClient.eval = originalEval; });