Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {});
}
};
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
14 changes: 9 additions & 5 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
});
Expand Down
Loading