diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e67975..e5d6ce6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,11 @@ name: Test -on: [push, pull_request, workflow_dispatch] +on: + pull_request: + push: + branches: + - main + workflow_dispatch: jobs: test: diff --git a/CHANGELOG.md b/CHANGELOG.md index 895ac8f..69f9a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.0] - 2026-08-04 +See the [v4 to v5 migration guide](docs/v4-to-v5.md). +### Added +- `pettyCache.close()` stops the background refresh intervals started by `fetchAndRefresh` and gracefully closes the Redis client connection. +### Changed +- Callback support is removed. Every function returns a promise; passing a callback rejects with a `TypeError`. +- Cache-miss functions and `retrieveOrCreate`'s `size` option must be async (or plain-return) functions; callback-style functions are no longer supported. +- Upgraded the `redis` client from v3 to v6. The client is connected automatically, and injected clients must be node-redis v6 clients. +- The constructor takes a node-redis v6 options object, passed to `redis.createClient()` untouched. The `(port, [host, [options]])` signature and node-redis v3 option names (`auth_pass`, `host`, `port`, `enable_offline_queue`, and friends) are no longer supported. ## [4.0.1] - 2026-08-05 ### Fixed - A callback that throws is no longer invoked a second time with its own error. Callbacks are now invoked on the next tick, outside of the promise chain, so a throw from within a callback surfaces as an uncaught exception instead of being swallowed. Affects every callback-style function. diff --git a/CLAUDE.md b/CLAUDE.md index c774041..2d93ea3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ npm test npm run coveralls # Run a single test file -npx mocha test/index.js +node --test --test-force-exit --test-reporter=spec test/index.js ``` ### Linting @@ -36,11 +36,10 @@ npm install ### Core Structure - **Main Entry**: `index.js` - Contains the PettyCache class and all public API methods -- **Test Suite**: `test/index.js` - Comprehensive Mocha test suite testing all cache operations, mutex, and semaphore functionality +- **Test Suite**: `test/index.js` - Comprehensive node:test suite testing all cache operations, mutex, and semaphore functionality - **Dependencies**: - - `redis` (v3.1.0) - Redis client for distributed caching + - `redis` (v6) - Redis client for distributed caching; petty-cache connects it automatically - `memory-cache` (v0.2.0) - In-memory cache for recently accessed data - - `async` (v3.2.6) - Async utility functions - `lock` (v1.1.0) - Local locking mechanism ### Key Design Patterns diff --git a/README.md b/README.md index 5446b94..95fae6e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A cache module for Node.js that uses a two-level cache (in-memory cache for rece Also includes mutex and semaphore distributed locking primitives. -As of v4, every function supports both promises (async/await) and callbacks — omit the callback to receive a promise. Callback support is deprecated and will be removed in v5; callback-style usage emits a Node.js `DeprecationWarning`. +As of v5, every function returns a promise and callbacks are no longer supported — passing a callback rejects with a `TypeError`. Cache-miss functions must be async (or plain-return) functions. If you need callback support, use v4, which supports both styles and emits deprecation warnings for callback usage. See the [v4 to v5 migration guide](docs/v4-to-v5.md). ## Features @@ -36,29 +36,28 @@ const PettyCache = require('petty-cache'); const pettyCache = new PettyCache(); // Fetch some data -pettyCache.fetch('key', function(callback) { +const value = await pettyCache.fetch('key', async () => { // This function is called on a cache miss - fs.readFile('file.txt', callback); -}, function(err, value) { - // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function - console.log(value); + return await fs.readFile('file.txt'); }); ``` ## API -### new PettyCache([port, [host, [options]]]) +### new PettyCache([options]) -Creates a new petty-cache client. `port`, `host`, and `options` are passed directly to [redis.createClient()](https://www.npmjs.com/package/redis#rediscreateclient). +Creates a new petty-cache client backed by [node-redis](https://www.npmjs.com/package/redis) v6 and connects it automatically. `options` is passed to [redis.createClient()](https://www.npmjs.com/package/redis) untouched. + +v4's `(port, host, options)` signature and node-redis v3 option names are not supported; see the [v4 to v5 migration guide](docs/v4-to-v5.md). **Example** ```javascript -const pettyCache = new PettyCache(6379, 'localhost', { auth_pass: 'secret' }); +const pettyCache = new PettyCache({ password: 'secret', socket: { host: 'localhost', port: 6379 } }); ``` ### new PettyCache(RedisClient) -Alternatively, you can inject your own [RedisClient](https://www.npmjs.com/package/redis) into Petty Cache. +Alternatively, you can inject your own node-redis v6 client into Petty Cache. If the client isn't already connected, petty-cache connects it. **Example** ```javascript @@ -66,27 +65,12 @@ const redisClient = redis.createClient(); const pettyCache = new PettyCache(redisClient); ``` -### pettyCache.bulkFetch(keys, cacheMissFunction, [options, [callback]]) +### pettyCache.bulkFetch(keys, cacheMissFunction, [options]) -Attempts to retrieve the values of the keys specified in the `keys` array. Any keys that aren't found are passed to cacheMissFunction as an array along with a callback that takes an error and an object, expecting the keys of the object to be the keys passed to `cacheMissFunction` and the values to be the values that should be stored in cache for the corresponding key. Either way, the resulting error or key-value hash of all requested keys is passed to `callback`. Supports both callbacks and promises. +Attempts to retrieve the values of the keys specified in the `keys` array. Any keys that aren't found are passed to cacheMissFunction as an array. `cacheMissFunction` should retrieve the expected values for the missing keys from another source and return an object, expecting the keys of the object to be the keys passed to `cacheMissFunction` and the values to be the values that should be stored in cache for the corresponding key. Resolves with a key-value hash of all requested keys. **Example** -```javascript -// Let's assume a and b are already cached as 1 and 2 -pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function(keys, callback) { - const results = {}; - - keys.forEach(function(key) { - results[key] = key.toUpperCase(); - }); - - callback(null, results); -}, function(err, values) { - console.log(values); // {a: 1, b: 2, c: 'C', d: 'D'} -}); -``` - ```javascript // Let's assume a and b are already cached as 1 and 2 const values = await pettyCache.bulkFetch(['a', 'b', 'c', 'd'], async (keys) => { @@ -120,36 +104,22 @@ console.log(values); // {a: 1, b: 2, c: 'C', d: 'D'} } ``` -### pettyCache.bulkGet(keys, [callback]) +### pettyCache.bulkGet(keys) -Attempts to retrieve the values of the keys specified in the `keys` array. Returns a key-value hash of all specified keys with either the corresponding values from cache or `null` if a key was not found. Supports both callbacks and promises. +Attempts to retrieve the values of the keys specified in the `keys` array. Resolves with a key-value hash of all specified keys with either the corresponding values from cache or `null` if a key was not found. **Example** -```javascript -pettyCache.bulkGet(['key1', 'key2', 'key3'], function(err, values) { - console.log(values); -}); -``` - ```javascript const values = await pettyCache.bulkGet(['key1', 'key2', 'key3']); ``` -### pettyCache.bulkSet(values, [options, [callback]]) +### pettyCache.bulkSet(values, [options]) -Unconditionally sets the values for the specified keys. Supports both callbacks and promises. +Unconditionally sets the values for the specified keys. **Example** -```javascript -pettyCache.bulkSet({ key1: 'one', key2: 2, key3: 'three' }, function(err) { - if (err) { - // Handle error - } -}); -``` - ```javascript await pettyCache.bulkSet({ key1: 'one', key2: 2, key3: 'three' }); ``` @@ -172,49 +142,31 @@ await pettyCache.bulkSet({ key1: 'one', key2: 2, key3: 'three' }); } ``` -### pettyCache.del(key, [callback]) +### pettyCache.close() -Deletes a value from both the in-memory cache and Redis. Supports both callbacks and promises. +Stops the background refresh intervals started by `pettyCache.fetchAndRefresh` and gracefully closes the Redis client connection. **Example** ```javascript -pettyCache.del('key', function(err) { - if (err) { - // Handle redis error - } -}); -``` - -```javascript -await pettyCache.del('key'); +await pettyCache.close(); ``` -### pettyCache.fetch(key, cacheMissFunction, [options, [callback]]) +### pettyCache.del(key) -Attempts to retrieve the value from cache at the specified key. If it doesn't exist, it executes the specified cacheMissFunction that takes two parameters: an error and a value. `cacheMissFunction` should retrieve the expected value for the key from another source and pass it to the given callback. Either way, the resulting error or value is passed to `callback`. Supports both callbacks and promises. +Deletes a value from both the in-memory cache and Redis. **Example** ```javascript -pettyCache.fetch('key', function(callback) { - // This function is called on a cache miss - fs.readFile('file.txt', callback); -}, function(err, value) { - // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function - console.log(value); -}); +await pettyCache.del('key'); ``` -```javascript -pettyCache.fetch('key', async () => { - // This function is called on a cache miss - return await fs.readFile('file.txt'); -}, function(err, value) { - // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function - console.log(value); -}); -``` +### pettyCache.fetch(key, cacheMissFunction, [options]) + +Attempts to retrieve the value from cache at the specified key. If it doesn't exist, it executes the specified cacheMissFunction, which should retrieve the expected value for the key from another source and return it. Either way, resolves with the resulting value. + +**Example** ```javascript const value = await pettyCache.fetch('key', async () => { @@ -241,21 +193,12 @@ const value = await pettyCache.fetch('key', async () => { } ``` -### pettyCache.fetchAndRefresh(key, cacheMissFunction, [options, [callback]]) +### pettyCache.fetchAndRefresh(key, cacheMissFunction, [options]) -Similar to `pettyCache.fetch` but this method continually refreshes the data in cache by executing the specified cacheMissFunction before the TTL expires. Supports both callbacks and promises. +Similar to `pettyCache.fetch` but this method continually refreshes the data in cache by executing the specified cacheMissFunction before the TTL expires. **Example** -```javascript -pettyCache.fetchAndRefresh('key', function(callback) { - // This function is called on a cache miss and every TTL/2 milliseconds - fs.readFile('file.txt', callback); -}, function(err, value) { - console.log(value); -}); -``` - ```javascript const value = await pettyCache.fetchAndRefresh('key', async () => { // This function is called on a cache miss and every TTL/2 milliseconds @@ -281,41 +224,26 @@ const value = await pettyCache.fetchAndRefresh('key', async () => { } ``` -### pettyCache.get(key, [callback]) +### pettyCache.get(key) -Attempts to retrieve the value from cache at the specified key. Returns `null` if the key doesn't exist. Supports both callbacks and promises. +Attempts to retrieve the value from cache at the specified key. Resolves with `null` if the key doesn't exist. **Example** -```javascript -pettyCache.get('key', function(err, value) { - // `value` contains the value of the key if it was found in the in-memory cache or Redis. `value` is `null` if the key was not found. - console.log(value); -}); -``` - ```javascript const value = await pettyCache.get('key'); ``` -### pettyCache.patch(key, value, [options, [callback]]) +### pettyCache.patch(key, value, [options]) -Updates an object at the given key with the property values provided. Sends an error to the callback if the key does not exist. Supports both callbacks and promises. +Updates an object at the given key with the property values provided. Rejects if the key does not exist. **Example** -```javascript -pettyCache.patch('key', { a: 1 }, function(err) { - if (err) { - // Handle redis or key not found error - } - - // The object stored at 'key' now has a property 'a' with the value 1. Its other values are intact. -}); -``` - ```javascript await pettyCache.patch('key', { a: 1 }); + +// The object stored at 'key' now has a property 'a' with the value 1. Its other values are intact. ``` **Options** @@ -336,20 +264,12 @@ await pettyCache.patch('key', { a: 1 }); } ``` -### pettyCache.set(key, value, [options, [callback]]) +### pettyCache.set(key, value, [options]) -Unconditionally sets a value for a given key. Supports both callbacks and promises. +Unconditionally sets a value for a given key. **Example** -```javascript -pettyCache.set('key', { a: 'b' }, function(err) { - if (err) { - // Handle redis error - } -}); -``` - ```javascript await pettyCache.set('key', { a: 'b' }); ``` @@ -390,20 +310,9 @@ const text = PettyCache.stringify({ a: null }); // '{"a":"__null"}' ## Mutex -### pettyCache.mutex.lock(key, [options, [callback]]) - -Attempts to acquire a distributed lock for the specified key. Optionally retries a specified number of times by waiting a specified amount of time between attempts. Supports both callbacks and promises. - -```javascript -pettyCache.mutex.lock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err) { - if (err) { - // We weren't able to acquire the lock (even after trying 5 times every 100 milliseconds). - } +### pettyCache.mutex.lock(key, [options]) - // We were able to acquire the lock. Do work and then unlock. - pettyCache.mutex.unlock('key'); -}); -``` +Attempts to acquire a distributed lock for the specified key. Optionally retries a specified number of times by waiting a specified amount of time between attempts. ```javascript await pettyCache.mutex.lock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }); @@ -424,17 +333,9 @@ await pettyCache.mutex.unlock('key'); } ``` -### pettyCache.mutex.unlock(key, [callback]) - -Releases the distributed lock for the specified key. Supports both callbacks and promises. +### pettyCache.mutex.unlock(key) -```javascript -pettyCache.mutex.unlock('key', function(err) { - if (err) { - // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. - } -}); -``` +Releases the distributed lock for the specified key. ```javascript await pettyCache.mutex.unlock('key'); @@ -448,48 +349,21 @@ Provides a pool of distributed locks. Once a consumer acquires a lock they have ```javascript // Create a new semaphore -pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }, function(err) { - if (err) { - // Aw, snap! We couldn't create the semaphore - } +await pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }); - // Acquire a lock from the semaphore's pool - pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err, index) { - if (err) { - // We couldn't acquire a lock from the semaphore's pool (even after trying 5 times every 100 milliseconds). - } - - // We were able to acquire a lock from the semaphore's pool. Do work and then release the lock. - pettyCache.semaphore.releaseLock('key', index, function(err) { - if (err) { - // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. - } - }); - - // Or, rather than releasing the lock back to the semaphore's pool you can mark the lock as "consumed" to prevent it from being used again. - pettyCache.semaphore.consumeLock('key', index, function(err) { - if (err) { - // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. - } - }); - }); -}); -``` +// Acquire a lock from the semaphore's pool +const index = await pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }); -### pettyCache.semaphore.acquireLock(key, [options, [callback]]) +// We were able to acquire a lock from the semaphore's pool. Do work and then release the lock. +await pettyCache.semaphore.releaseLock('key', index); -Attempts to acquire a lock from the semaphore's pool. Optionally retries a specified number of times by waiting a specified amount of time between attempts. Supports both callbacks and promises. +// Or, rather than releasing the lock back to the semaphore's pool you can mark the lock as "consumed" to prevent it from being used again. +await pettyCache.semaphore.consumeLock('key', index); +``` -```javascript -// Acquire a lock from the semaphore's pool -pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err, index) { - if (err) { - // We couldn't acquire a lock from the semaphore's pool (even after trying 5 times every 100 milliseconds). - } +### pettyCache.semaphore.acquireLock(key, [options]) - // We were able to acquire a lock from the semaphore's pool. Do work and then release the lock. -}); -``` +Attempts to acquire a lock from the semaphore's pool. Optionally retries a specified number of times by waiting a specified amount of time between attempts. Resolves with the index of the acquired slot. ```javascript const index = await pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }); @@ -507,84 +381,41 @@ const index = await pettyCache.semaphore.acquireLock('key', { retry: { interval: } ``` -### pettyCache.semaphore.consumeLock(key, index, [callback]) +### pettyCache.semaphore.consumeLock(key, index) -Mark the lock at the specified index as "consumed" to prevent it from being used again. Supports both callbacks and promises. - -```javascript -pettyCache.semaphore.consumeLock('key', index, function(err) { - if (err) { - // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. - } -}); -``` +Mark the lock at the specified index as "consumed" to prevent it from being used again. ```javascript await pettyCache.semaphore.consumeLock('key', index); ``` -### pettyCache.semaphore.expand(key, size, [callback]) - -Expand the number of locks in the specified semaphore's pool. Supports both callbacks and promises. +### pettyCache.semaphore.expand(key, size) -```javascript -pettyCache.semaphore.expand(key, 100, function(err) { - if (err) { - // We weren't able to expand the semaphore. - } -}); -``` +Expand the number of locks in the specified semaphore's pool. ```javascript await pettyCache.semaphore.expand(key, 100); ``` -### pettyCache.semaphore.releaseLock(key, index, [callback]) +### pettyCache.semaphore.releaseLock(key, index) -Releases the lock at the specified index back to the semaphore's pool so that it can be used again. Supports both callbacks and promises. - -```javascript -pettyCache.semaphore.releaseLock('key', index, function(err) { - if (err) { - // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. - } -}); -``` +Releases the lock at the specified index back to the semaphore's pool so that it can be used again. ```javascript await pettyCache.semaphore.releaseLock('key', index); ``` -### pettyCache.semaphore.reset(key, [callback]) +### pettyCache.semaphore.reset(key) -Resets all locks in the semaphore's pool to available, releasing them all (even those that have been marked as "consumed"). The pool keeps its current size, including any expansions. Supports both callbacks and promises. - -```javascript -pettyCache.semaphore.reset('key', function(err) { - if (err) { - // We weren't able to reset the semaphore. - } -}); -``` +Resets all locks in the semaphore's pool to available, releasing them all (even those that have been marked as "consumed"). The pool keeps its current size, including any expansions. Resolves with the reset pool. ```javascript const pool = await pettyCache.semaphore.reset('key'); ``` -### pettyCache.semaphore.retrieveOrCreate(key, [options, [callback]]) +### pettyCache.semaphore.retrieveOrCreate(key, [options]) -Retrieves a previously created semaphore or creates a new semaphore with the optionally specified number of locks in its pool. Supports both callbacks and promises. - -```javascript -// Create a new semaphore -pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }, function(err) { - if (err) { - // Aw, snap! We couldn't create the semaphore - } - - // Your semaphore was created. -}); -``` +Retrieves a previously created semaphore or creates a new semaphore with the optionally specified number of locks in its pool. Resolves with the semaphore's pool. ```javascript const semaphore = await pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }); @@ -594,6 +425,6 @@ const semaphore = await pettyCache.semaphore.retrieveOrCreate('key', { size: 10 ```javascript { - size: 1 || function() { const x = 1 + 1; callback(null, x); } // The number of locks to create in the semaphore's pool. Optionally, size can be a `callback(err, size)` function or an async function. + size: 1 // The number of locks to create in the semaphore's pool. Optionally, size can be an async function that resolves the size. } ``` diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md new file mode 100644 index 0000000..aeb6e87 --- /dev/null +++ b/docs/v4-to-v5.md @@ -0,0 +1,139 @@ +# Migrating from v4 to v5 + +v5 removes callback support entirely and upgrades the underlying Redis client from node-redis v3 to v6. v4 supports both callbacks and promises and emits a `DeprecationWarning` for every callback-style usage — migrate to v4 first, clear the warnings, and the jump to v5 is a version bump. + +## Finding callback usage + +On v4, each callback-style call emits a once-per-process `DeprecationWarning` naming the function. Run your service with `--trace-deprecation` to get a stack trace pointing at each call site: + +``` +node --trace-deprecation index.js +``` + +On v5, any remaining callback usage rejects with a `TypeError` instead of silently doing nothing: + +``` +TypeError: pettyCache.get: callbacks were removed in petty-cache v5. Use the returned promise instead. +``` + +## Method callbacks → promises + +Every function returns a promise. The final-callback parameter is gone from every signature. + +**Before** + +```javascript +pettyCache.get('key', function(err, value) { + if (err) { + // Handle error + } + + console.log(value); +}); +``` + +**After** + +```javascript +const value = await pettyCache.get('key'); +``` + +Error handling moves from the err-first parameter to `try`/`catch` (or `.catch`): + +```javascript +try { + await pettyCache.patch('key', { a: 1 }); +} catch (err) { + // Handle error (e.g. the key does not exist) +} +``` + +## Fire-and-forget calls + +Calls that previously omitted the callback to ignore the result now return a promise. An ignored rejected promise crashes modern Node.js processes, so handle it explicitly: + +**Before** + +```javascript +pettyCache.semaphore.reset(key); +``` + +**After** + +```javascript +pettyCache.semaphore.reset(key).catch(() => {}); +``` + +Prefer `await` where the calling code can be async. + +## Cache-miss functions must be async + +Callback-style cache-miss functions are no longer supported by `fetch`, `fetchAndRefresh`, and `bulkFetch`, and neither are callback-style `size` functions for `semaphore.retrieveOrCreate`. Return the value (or a promise) instead of calling a callback: + +**Before** + +```javascript +pettyCache.fetch('key', function(callback) { + fs.readFile('file.txt', callback); +}, function(err, value) { + console.log(value); +}); + +pettyCache.bulkFetch(['a', 'b'], function(keys, callback) { + callback(null, { a: 1, b: 2 }); +}, function(err, values) { + console.log(values); +}); +``` + +**After** + +```javascript +const value = await pettyCache.fetch('key', async () => { + return await fs.readFile('file.txt'); +}); + +const values = await pettyCache.bulkFetch(['a', 'b'], async (keys) => { + return { a: 1, b: 2 }; +}); +``` + +Plain-return functions (`() => value`) also work anywhere an async function is accepted. + +## Redis client v3 → v6 + +petty-cache now uses [node-redis](https://www.npmjs.com/package/redis) v6 and connects the client automatically. + +**The constructor takes a node-redis v6 options object.** v4's `(port, host, options)` signature and its node-redis v3 option names are both gone. + +**Before** + +```javascript +const pettyCache = new PettyCache(process.env.redisPort, process.env.redisHost, { auth_pass: process.env.redisPassword, enable_offline_queue: false }); +``` + +**After** + +```javascript +const pettyCache = new PettyCache({ disableOfflineQueue: true, password: process.env.redisPassword, socket: { host: process.env.redisHost, port: process.env.redisPort } }); +``` + +Option names that moved: + +| v3 | v6 | +| --- | --- | +| `auth_pass` | `password` | +| `host`, `port`, `path` | `socket.host`, `socket.port`, `socket.path` | +| `db` | `database` | +| `enable_offline_queue: false` | `disableOfflineQueue: true` | +| `connect_timeout` | `socket.connectTimeout` | +| `socket_keepalive`, `socket_initial_delay` | `socket.keepAlive`, `socket.keepAliveInitialDelay` | +| `family` | `socket.family` (a number, not `'IPv4'`) | +| `tls: {...}` | `socket.tls: true` plus the TLS options on `socket` | +| `retry_strategy` | `socket.reconnectStrategy` | + +**Injected clients must be node-redis v6 clients.** `new PettyCache(redisClient)` requires a client created by `redis@6`'s `createClient()`. petty-cache connects it if it isn't already connected. + +## New: close() + +v5 adds a shutdown API. `await pettyCache.close()` stops the background refresh intervals started by `fetchAndRefresh` and gracefully closes the Redis client connection. diff --git a/index.js b/index.js index d707675..5dd6361 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,9 @@ const timers = require('node:timers/promises'); -const util = require('node:util'); const lock = require('lock').Lock(); const memoryCache = require('memory-cache'); const redis = require('redis'); -const deprecationWarnings = new Set(); - /** * Acquires the in-process lock for the given key and resolves once it's held. * @param {string} key @@ -19,55 +16,16 @@ function acquireLock(key) { } /** - * Emits a once-per-process DeprecationWarning for callback-style usage. - * @param {string} name - The public method name shown in the warning. - * @param {string} [message] - Overrides the standard callback deprecation message. + * Throws if any of the given arguments is a function, catching legacy callback-style calls. + * @param {string} name - The public method name shown in the error. + * @param {...*} args - Arguments beyond the method's supported signature. */ -function deprecateCallback(name, message) { - if (deprecationWarnings.has(name)) { - return; +function assertNoCallback(name, ...args) { + if (args.some(arg => typeof arg === 'function')) { + throw new TypeError(`${name}: callbacks were removed in petty-cache v5. Use the returned promise instead.`); } - - deprecationWarnings.add(name); - process.emitWarning(message || `${name}: callbacks are deprecated and will be removed in petty-cache v5. Omit the callback to receive a promise.`, 'DeprecationWarning'); } -/** - * Executes a cache-miss function, supporting both async and callback signatures. - * @param {Function} func - Use func(...args, callback) for callbacks or async func(...args) for promises. - * @param {...*} args - Arguments to pass to func ahead of any callback. - * @returns {Promise<*>} Resolves with the value produced by func. - */ -async function executeFunc(func, ...args) { - // If the function doesn't declare a parameter beyond the provided arguments, there wasn't a callback provided - if (func.length <= args.length) { - return func(...args); - } - - // If the function declares an additional parameter, there was a callback provided - deprecateCallback('callback-style functions', 'Callback-style functions passed to petty-cache are deprecated and will be removed in petty-cache v5. Use an async function instead.'); - - return new Promise((resolve, reject) => { - func(...args, (err, data) => { - if (err) { - return reject(err); - } - - resolve(data); - }); - }); -} - -/** - * Invokes a callback with the outcome of the specified promise. The callback is invoked on the next - * tick, outside of the promise chain, so that it's invoked exactly once and a throw from within the - * callback surfaces as an uncaught exception instead of rejecting the chain. - * @param {Promise} promise - The promise to bridge to the callback. - * @param {Function} callback - The caller's callback(err, result). - */ -function invokeCallback(promise, callback) { - promise.then(result => process.nextTick(callback, null, result), err => process.nextTick(callback, err)); -} /** * Returns a random integer between min and max, inclusive. @@ -83,31 +41,29 @@ function random(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } + /** * Creates a new PettyCache instance backed by Redis. - * Accepts the same arguments as redis.createClient(), or an existing RedisClient instance. - * @param {...*} args - Either a RedisClient instance, or arguments forwarded to redis.createClient(). + * @param {RedisClient|Object} [options] - A node-redis v6 client, or options for redis.createClient(). */ -function PettyCache() { +function PettyCache(options) { const intervals = {}; let redisClient; - if (arguments[0] instanceof redis.RedisClient) { - redisClient = arguments[0]; + if (options instanceof redis.RedisClient) { + redisClient = options; } else { - redisClient = redis.createClient(...arguments); + redisClient = redis.createClient(options); } //eslint-disable-next-line no-console redisClient.on('error', err => console.warn(`Warning: Redis reported a client error: ${err}`)); - // Promisify per call rather than once at construction so that wrappers applied to the - // client's methods later (APM instrumentation, test stubs) are respected - const delAsync = (...args) => util.promisify(redisClient.del).apply(redisClient, args); - const getAsync = (...args) => util.promisify(redisClient.get).apply(redisClient, args); - const mgetAsync = (...args) => util.promisify(redisClient.mget).apply(redisClient, args); - const psetexAsync = (...args) => util.promisify(redisClient.psetex).apply(redisClient, args); - const setAsync = (...args) => util.promisify(redisClient.set).apply(redisClient, args); + // Connect automatically; commands issued while the client is connecting are queued. + // Connection errors surface through the error event above and as rejected commands. + if (!redisClient.isOpen) { + redisClient.connect().catch(() => {}); + } /** * Fetches multiple keys from Redis. @@ -116,7 +72,7 @@ function PettyCache() { */ async function bulkGetFromRedis(keys) { // Try to get values from Redis - const data = await mgetAsync(keys); + const data = await redisClient.mGet(keys); const values = {}; @@ -165,7 +121,7 @@ function PettyCache() { */ async function getFromRedis(key) { // Try to get value from Redis - const data = await getAsync(key); + const data = await redisClient.get(key); // Return if the key wasn't found in Redis if (data === null) { @@ -208,235 +164,222 @@ function PettyCache() { /** * Returns data from cache for each key if available; otherwise executes func for the missing keys - * and stores the results in cache before returning. Supports both callback and promise styles. + * and stores the results in cache before returning. * @param {Array} keys - An array of cache keys. - * @param {Function} func - Called with the missing keys. Use func(keys, callback) for callbacks or async func(keys) for promises. + * @param {Function} func - Called with the missing keys: async func(keys). * @param {Object} [options] - Optional settings. * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err, values). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with an object mapping each key to its cached value. + * @returns {Promise} Resolves with an object mapping each key to its cached value. */ - this.bulkFetch = (keys, func, options = {}, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; - } + this.bulkFetch = async (keys, func, options = {}, ...rest) => { + assertNoCallback('pettyCache.bulkFetch', options, ...rest); - const executor = async () => { - // If there aren't any keys, return - if (!keys.length) { - return {}; - } - - const _keys = Array.from(new Set(keys)); - const values = {}; + // If there aren't any keys, return + if (!keys.length) { + return {}; + } - // Try to get values from memory cache - for (let i = _keys.length - 1; i >= 0; i--) { - const key = _keys[i]; - const result = getFromMemoryCache(key); + const _keys = Array.from(new Set(keys)); + const values = {}; - if (result.exists) { - values[key] = result.value; - _keys.splice(i, 1); - } - } + // Try to get values from memory cache + for (let i = _keys.length - 1; i >= 0; i--) { + const key = _keys[i]; + const result = getFromMemoryCache(key); - // If there aren't any keys left, return - if (!_keys.length) { - return values; + if (result.exists) { + values[key] = result.value; + _keys.splice(i, 1); } + } - // Try to get values from Redis - const results = await bulkGetFromRedis(_keys); + // If there aren't any keys left, return + if (!_keys.length) { + return values; + } - for (let i = _keys.length - 1; i >= 0; i--) { - const key = _keys[i]; - const result = results[key]; + // Try to get values from Redis + const results = await bulkGetFromRedis(_keys); - if (result.exists) { - _keys.splice(i, 1); - values[key] = result.value; + for (let i = _keys.length - 1; i >= 0; i--) { + const key = _keys[i]; + const result = results[key]; - // Store value in memory cache with a short expiration - memoryCache.put(key, result.value, random(2000, 5000)); - } - } + if (result.exists) { + _keys.splice(i, 1); + values[key] = result.value; - // If there aren't any keys left, return - if (!_keys.length) { - return values; + // Store value in memory cache with a short expiration + memoryCache.put(key, result.value, random(2000, 5000)); } + } - // Execute the specified function for remaining keys - const data = await executeFunc(func, _keys); + // If there aren't any keys left, return + if (!_keys.length) { + return values; + } - Object.keys(data).forEach(key => values[key] = data[key]); + // Execute the specified function for remaining keys + const data = await func(_keys); - await this.bulkSet(data, options); + Object.keys(data).forEach(key => values[key] = data[key]); - return values; - }; + await this.bulkSet(data, options); - if (callback) { - deprecateCallback('pettyCache.bulkFetch'); - invokeCallback(executor(), callback); - } else { - return executor(); - } + return values; }; /** - * Gets cached values for an array of keys. Supports both callback and promise styles. + * Gets cached values for an array of keys. * @param {Array} keys - An array of cache keys. - * @param {Function} [callback] - Optional callback(err, values). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with an object mapping each key to its value, or null if not found. + * @returns {Promise} Resolves with an object mapping each key to its value, or null if not found. */ - this.bulkGet = (keys, callback) => { - const executor = async () => { - // If there aren't any keys, return - if (!keys.length) { - return {}; - } + this.bulkGet = async (keys, ...rest) => { + assertNoCallback('pettyCache.bulkGet', ...rest); - const _keys = Array.from(new Set(keys)); - const values = {}; + // If there aren't any keys, return + if (!keys.length) { + return {}; + } - // Try to get values from memory cache - for (let i = _keys.length - 1; i >= 0; i--) { - const key = _keys[i]; - const result = getFromMemoryCache(key); + const _keys = Array.from(new Set(keys)); + const values = {}; - if (result.exists) { - values[key] = result.value; - _keys.splice(i, 1); - } - } + // Try to get values from memory cache + for (let i = _keys.length - 1; i >= 0; i--) { + const key = _keys[i]; + const result = getFromMemoryCache(key); - // If there aren't any keys left, return - if (!_keys.length) { - return values; + if (result.exists) { + values[key] = result.value; + _keys.splice(i, 1); } + } - // Try to get values from Redis - const results = await bulkGetFromRedis(_keys); - - for (let i = 0; i < _keys.length; i++) { - const key = _keys[i]; - const result = results[key]; + // If there aren't any keys left, return + if (!_keys.length) { + return values; + } - if (!result.exists) { - values[key] = null; - continue; - } + // Try to get values from Redis + const results = await bulkGetFromRedis(_keys); - values[key] = result.value; + for (let i = 0; i < _keys.length; i++) { + const key = _keys[i]; + const result = results[key]; - // Store value in memory cache with a short expiration - memoryCache.put(key, result.value, random(2000, 5000)); + if (!result.exists) { + values[key] = null; + continue; } - return values; - }; + values[key] = result.value; - if (callback) { - deprecateCallback('pettyCache.bulkGet'); - invokeCallback(executor(), callback); - } else { - return executor(); + // Store value in memory cache with a short expiration + memoryCache.put(key, result.value, random(2000, 5000)); } + + return values; }; /** - * Sets multiple key/value pairs in cache simultaneously. Supports both callback and promise styles. + * Sets multiple key/value pairs in cache simultaneously. * @param {Object} values - An object mapping cache keys to their values. * @param {Object} [options] - Optional settings. * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - this.bulkSet = (values, options = {}, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; - } + this.bulkSet = async (values, options = {}, ...rest) => { + assertNoCallback('pettyCache.bulkSet', options, ...rest); - const executor = async () => { - // Get TTL based on specified options - const ttl = getTtl(options); + // Get TTL based on specified options + const ttl = getTtl(options); - // Redis does not have a MSETEX command so we batch commands: http://redis.js.org/#api-clientbatchcommands - const batch = redisClient.batch(); + // Redis does not have an MSETEX command; individual PSETEX commands issued in the + // same tick are automatically pipelined into a single round trip by node-redis + await Promise.all(Object.keys(values).map(key => { + const value = values[key]; - Object.keys(values).forEach(key => { - const value = values[key]; + // Store value in memory cache with a short expiration + memoryCache.put(key, value, random(2000, 5000)); - // Store value in memory cache with a short expiration - memoryCache.put(key, value, random(2000, 5000)); + return redisClient.pSetEx(key, random(ttl.min, ttl.max), PettyCache.stringify(value)); + })); + }; - // Add Redis command - batch.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value)); - }); + /** + * Stops the background refresh intervals started by fetchAndRefresh and gracefully + * closes the Redis client connection. + * @returns {Promise} + */ + this.close = async (...rest) => { + assertNoCallback('pettyCache.close', ...rest); - await util.promisify(batch.exec).call(batch); - }; + Object.keys(intervals).forEach(key => { + clearInterval(intervals[key]); + delete intervals[key]; + }); - if (callback) { - deprecateCallback('pettyCache.bulkSet'); - invokeCallback(executor(), callback); - } else { - return executor(); + if (redisClient.isOpen) { + await redisClient.close(); } }; /** - * Deletes a key from both the memory cache and Redis. Supports both callback and promise styles. + * Deletes a key from both the memory cache and Redis. * @param {string} key - The cache key to delete. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - this.del = (key, callback) => { - const executor = async () => { - await delAsync(key); - memoryCache.del(key); - }; + this.del = async (key, ...rest) => { + assertNoCallback('pettyCache.del', ...rest); - if (callback) { - deprecateCallback('pettyCache.del'); - invokeCallback(executor(), callback); - } else { - return executor(); - } + await redisClient.del(key); + memoryCache.del(key); }; /** * Returns data from cache if available; otherwise executes func, stores the result, and returns it. - * Uses double-checked locking to prevent cache stampedes. Supports async and callback func signatures, - * and both callback and promise styles. + * Uses double-checked locking to prevent cache stampedes. * @param {string} key - The cache key. - * @param {Function} func - Called on cache miss. Use func(callback) for callbacks or async func() for promises. + * @param {Function} func - Called on cache miss: async func(). * @param {Object} [options] - Optional settings. * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err, value). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the cached or newly fetched value. + * @returns {Promise<*>} Resolves with the cached or newly fetched value. */ - this.fetch = (key, func, options = {}, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; + this.fetch = async (key, func, options = {}, ...rest) => { + assertNoCallback('pettyCache.fetch', options, ...rest); + + // Try to get value from memory cache + let result = getFromMemoryCache(key); + + // Return value from memory cache if it exists + if (result.exists) { + return result.value; } - const executor = async () => { + // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking + const releaseMemoryCacheLock = await acquireLock(`fetch-memory-cache-lock-${key}`); + + try { // Try to get value from memory cache - let result = getFromMemoryCache(key); + result = getFromMemoryCache(key); // Return value from memory cache if it exists if (result.exists) { return result.value; } + // Try to get value from Redis + result = await getFromRedis(key); + + // Return value from Redis if it exists + if (result.exists) { + memoryCache.put(key, result.value, random(2000, 5000)); + return result.value; + } + // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking - const releaseMemoryCacheLock = await acquireLock(`fetch-memory-cache-lock-${key}`); + const releaseRedisLock = await acquireLock(`fetch-redis-lock-${key}`); try { // Try to get value from memory cache @@ -456,65 +399,31 @@ function PettyCache() { return result.value; } - // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking - const releaseRedisLock = await acquireLock(`fetch-redis-lock-${key}`); - - try { - // Try to get value from memory cache - result = getFromMemoryCache(key); - - // Return value from memory cache if it exists - if (result.exists) { - return result.value; - } - - // Try to get value from Redis - result = await getFromRedis(key); - - // Return value from Redis if it exists - if (result.exists) { - memoryCache.put(key, result.value, random(2000, 5000)); - return result.value; - } - - // Execute the specified function and place the results in cache before returning the data - const data = await executeFunc(func); + // Execute the specified function and place the results in cache before returning the data + const data = await func(); - await this.set(key, data, options); + await this.set(key, data, options); - return data; - } finally { - releaseRedisLock(); - } + return data; } finally { - releaseMemoryCacheLock(); + releaseRedisLock(); } - }; - - if (callback) { - deprecateCallback('pettyCache.fetch'); - invokeCallback(executor(), callback); - } else { - return executor(); + } finally { + releaseMemoryCacheLock(); } }; /** * Like fetch(), but also sets up a background interval to proactively refresh the cached value - * before it expires, preventing cache misses under sustained load. Supports async and callback - * func signatures, and both callback and promise styles. + * before it expires, preventing cache misses under sustained load. * @param {string} key - The cache key. - * @param {Function} func - Called on cache miss and on each refresh interval. Use func(callback) for callbacks or async func() for promises. + * @param {Function} func - Called on cache miss and on each refresh interval: async func(). * @param {Object} [options] - Optional settings. * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err, value). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the cached or newly fetched value. + * @returns {Promise<*>} Resolves with the cached or newly fetched value. */ - this.fetchAndRefresh = (key, func, options = {}, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; - } + this.fetchAndRefresh = async (key, func, options = {}, ...rest) => { + assertNoCallback('pettyCache.fetchAndRefresh', options, ...rest); // Get TTL based on specified options const ttl = getTtl(options); @@ -532,7 +441,7 @@ function PettyCache() { // Execute the specified function and update cache, trying again next interval on failure try { - const data = await executeFunc(func); + const data = await func(); await this.set(key, data, options); } catch (err) { @@ -541,87 +450,67 @@ function PettyCache() { }, delay); } - const promise = this.fetch(key, func, options); - - if (callback) { - deprecateCallback('pettyCache.fetchAndRefresh'); - invokeCallback(promise, callback); - } else { - return promise; - } + return this.fetch(key, func, options); }; /** - * Gets a cached value. Supports both callback and promise styles. + * Gets a cached value. * @param {string} key - The cache key. - * @param {Function} [callback] - Optional callback(err, value). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the cached value, or null if not found. + * @returns {Promise<*>} Resolves with the cached value, or null if not found. */ - this.get = (key, callback) => { - const executor = async () => { + this.get = async (key, ...rest) => { + assertNoCallback('pettyCache.get', ...rest); + + // Try to get value from memory cache + let result = getFromMemoryCache(key); + + // Return value from memory cache if it exists + if (result.exists) { + return result.value; + } + + // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking + const releaseMemoryCacheLock = await acquireLock(`get-memory-cache-lock-${key}`); + + try { // Try to get value from memory cache - let result = getFromMemoryCache(key); + result = getFromMemoryCache(key); // Return value from memory cache if it exists if (result.exists) { return result.value; } - // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking - const releaseMemoryCacheLock = await acquireLock(`get-memory-cache-lock-${key}`); - - try { - // Try to get value from memory cache - result = getFromMemoryCache(key); - - // Return value from memory cache if it exists - if (result.exists) { - return result.value; - } - - // Try to get value from Redis - result = await getFromRedis(key); - - // Return null if the key wasn't found in Redis - if (!result.exists) { - return null; - } - - // Store value in memory cache with a short expiration - memoryCache.put(key, result.value, random(2000, 5000)); + // Try to get value from Redis + result = await getFromRedis(key); - return result.value; - } finally { - releaseMemoryCacheLock(); + // Return null if the key wasn't found in Redis + if (!result.exists) { + return null; } - }; - if (callback) { - deprecateCallback('pettyCache.get'); - invokeCallback(executor(), callback); - } else { - return executor(); + // Store value in memory cache with a short expiration + memoryCache.put(key, result.value, random(2000, 5000)); + + return result.value; + } finally { + releaseMemoryCacheLock(); } }; this.mutex = { /** - * Acquires a distributed mutex lock in Redis. Supports both callback and promise styles. + * Acquires a distributed mutex lock in Redis. * @param {string} key - The lock key. * @param {Object} [options] - Optional settings. * @param {number} [options.ttl=1000] - Lock TTL in ms. * @param {Object} [options.retry] - Retry options. * @param {number} [options.retry.times=1] - Number of acquisition attempts. * @param {number} [options.retry.interval=100] - Delay between retries in ms. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - lock: (key, options = {}, callback) => { - // Options are optional - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } + lock: async (key, options = {}, ...rest) => { + assertNoCallback('pettyCache.mutex.lock', options, ...rest); options.retry = Object.hasOwn(options, 'retry') ? options.retry : {}; options.retry.interval = Object.hasOwn(options.retry, 'interval') ? options.retry.interval : 100; @@ -629,7 +518,7 @@ function PettyCache() { options.ttl = Object.hasOwn(options, 'ttl') ? options.ttl : 1000; const attempt = async () => { - const res = await setAsync(key, '1', 'NX', 'PX', options.ttl); + const res = await redisClient.set(key, '1', { NX: true, PX: options.ttl }); if (!res) { throw new Error(); @@ -640,105 +529,68 @@ function PettyCache() { } }; - const executor = async () => { - let attempts = options.retry.times; + let attempts = options.retry.times; - while (attempts > 1) { - try { - return await attempt(); - } catch (err) { - attempts--; - await timers.setTimeout(options.retry.interval); - } + while (attempts > 1) { + try { + return await attempt(); + } catch (err) { + attempts--; + await timers.setTimeout(options.retry.interval); } - - return attempt(); - }; - - if (callback) { - deprecateCallback('pettyCache.mutex.lock'); - invokeCallback(executor(), callback); - } else { - return executor(); } + + return attempt(); }, /** - * Releases a distributed mutex lock in Redis. Supports both callback and promise styles. + * Releases a distributed mutex lock in Redis. * @param {string} key - The lock key to release. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - unlock: (key, callback) => { - const executor = async () => { - await delAsync(key); - }; + unlock: async (key, ...rest) => { + assertNoCallback('pettyCache.mutex.unlock', ...rest); - if (callback) { - deprecateCallback('pettyCache.mutex.unlock'); - invokeCallback(executor(), callback); - } else { - return executor(); - } + await redisClient.del(key); } }; /** * Updates specific properties of a cached object without replacing the whole value. - * Supports both callback and promise styles. * @param {string} key - The cache key of the object to patch. * @param {Object} value - Properties to merge into the cached object. * @param {Object} [options] - Optional settings passed to set(). * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - this.patch = (key, value, options = {}, callback) => { - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - const executor = async () => { - const data = await this.get(key); + this.patch = async (key, value, options = {}, ...rest) => { + assertNoCallback('pettyCache.patch', options, ...rest); - if (!data) { - throw new Error(`Key ${key} does not exist`); - } - - for (let k in value) { - data[k] = value[k]; - } + const data = await this.get(key); - await this.set(key, data, options); - }; + if (!data) { + throw new Error(`Key ${key} does not exist`); + } - if (callback) { - deprecateCallback('pettyCache.patch'); - invokeCallback(executor(), callback); - } else { - return executor(); + for (let k in value) { + data[k] = value[k]; } + + await this.set(key, data, options); }; this.semaphore = { /** * Acquires a slot in an existing semaphore pool. Retries if no slot is currently available. - * Supports both callback and promise styles. * @param {string} key - The semaphore key. * @param {Object} [options] - Optional settings. * @param {number} [options.ttl=1000] - Slot TTL in ms; expired slots may be reclaimed. * @param {Object} [options.retry] - Retry options. * @param {number} [options.retry.times=1] - Number of acquisition attempts. * @param {number} [options.retry.interval=100] - Delay between retries in ms. - * @param {Function} [callback] - Optional callback(err, index). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the acquired slot index. + * @returns {Promise} Resolves with the acquired slot index. */ - acquireLock: (key, options = {}, callback) => { - // Options are optional - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } + acquireLock: async (key, options = {}, ...rest) => { + assertNoCallback('pettyCache.semaphore.acquireLock', options, ...rest); options.retry = Object.hasOwn(options, 'retry') ? options.retry : {}; options.retry.interval = Object.hasOwn(options.retry, 'interval') ? options.retry.interval : 100; @@ -750,7 +602,7 @@ function PettyCache() { await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); try { - const data = await getAsync(key); + const data = await redisClient.get(key); // If we don't have a previously created semaphore, return error if (!data) { @@ -773,7 +625,7 @@ function PettyCache() { pool[index] = { status: 'acquired', ttl: Date.now() + options.ttl }; - await setAsync(key, JSON.stringify(pool)); + await redisClient.set(key, JSON.stringify(pool)); return index; } finally { @@ -782,297 +634,225 @@ function PettyCache() { } }; - const executor = async () => { - let attempts = options.retry.times; + let attempts = options.retry.times; - while (attempts > 1) { - try { - return await attempt(); - } catch (err) { - attempts--; - await timers.setTimeout(options.retry.interval); - } + while (attempts > 1) { + try { + return await attempt(); + } catch (err) { + attempts--; + await timers.setTimeout(options.retry.interval); } - - return attempt(); - }; - - if (callback) { - deprecateCallback('pettyCache.semaphore.acquireLock'); - invokeCallback(executor(), callback); - } else { - return executor(); } + + return attempt(); }, /** * Permanently consumes a semaphore slot, marking it consumed rather than available. - * Ensures at least one slot always remains non-consumed. Supports both callback and promise styles. + * Ensures at least one slot always remains non-consumed. * @param {string} key - The semaphore key. * @param {number} index - The slot index to consume. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - consumeLock: (key, index, callback) => { - const executor = async () => { - // Mutex lock around semaphore - await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); + consumeLock: async (key, index, ...rest) => { + assertNoCallback('pettyCache.semaphore.consumeLock', ...rest); - try { - const data = await getAsync(key); + // Mutex lock around semaphore + await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - // If we don't have a previously created semaphore, return error - if (!data) { - throw new Error(`Semaphore ${key} doesn't exist.`); - } + try { + const data = await redisClient.get(key); - const pool = JSON.parse(data); + // If we don't have a previously created semaphore, return error + if (!data) { + throw new Error(`Semaphore ${key} doesn't exist.`); + } - // Ensure index exists. - if (pool.length <= index) { - throw new Error(`Index ${index} for semaphore ${key} is invalid.`); - } + const pool = JSON.parse(data); - pool[index] = { status: 'consumed' }; + // Ensure index exists. + if (pool.length <= index) { + throw new Error(`Index ${index} for semaphore ${key} is invalid.`); + } - // Ensure at least one slot isn't consumed - if (pool.every(s => s.status === 'consumed')) { - pool[index] = { status: 'available' }; - } + pool[index] = { status: 'consumed' }; - await setAsync(key, JSON.stringify(pool)); - } finally { - // Unlock errors are ignored; the mutex lock expires via its TTL - await this.mutex.unlock(`lock:${key}`).catch(() => {}); + // Ensure at least one slot isn't consumed + if (pool.every(s => s.status === 'consumed')) { + pool[index] = { status: 'available' }; } - }; - if (callback) { - deprecateCallback('pettyCache.semaphore.consumeLock'); - invokeCallback(executor(), callback); - } else { - return executor(); + await redisClient.set(key, JSON.stringify(pool)); + } finally { + // Unlock errors are ignored; the mutex lock expires via its TTL + await this.mutex.unlock(`lock:${key}`).catch(() => {}); } }, /** * Increases the size of an existing semaphore pool. Cannot shrink a pool. - * Supports both callback and promise styles. * @param {string} key - The semaphore key. * @param {number} size - The desired pool size (must be >= current size). - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - expand: (key, size, callback) => { - const executor = async () => { - // Mutex lock around semaphore - await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - - try { - const data = await getAsync(key); + expand: async (key, size, ...rest) => { + assertNoCallback('pettyCache.semaphore.expand', ...rest); - // If we don't have a previously created semaphore, return error - if (!data) { - throw new Error(`Semaphore ${key} doesn't exist.`); - } + // Mutex lock around semaphore + await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - let pool = JSON.parse(data); + try { + const data = await redisClient.get(key); - if (pool.length > size) { - throw new Error(`Cannot shrink pool, size is ${pool.length} and you requested a size of ${size}.`); - } + // If we don't have a previously created semaphore, return error + if (!data) { + throw new Error(`Semaphore ${key} doesn't exist.`); + } - if (pool.length === size) { - return; - } + let pool = JSON.parse(data); - pool = pool.concat(Array(size - pool.length).fill({ status: 'available' })); + if (pool.length > size) { + throw new Error(`Cannot shrink pool, size is ${pool.length} and you requested a size of ${size}.`); + } - await setAsync(key, JSON.stringify(pool)); - } finally { - // Unlock errors are ignored; the mutex lock expires via its TTL - await this.mutex.unlock(`lock:${key}`).catch(() => {}); + if (pool.length === size) { + return; } - }; - if (callback) { - deprecateCallback('pettyCache.semaphore.expand'); - invokeCallback(executor(), callback); - } else { - return executor(); + pool = pool.concat(Array(size - pool.length).fill({ status: 'available' })); + + await redisClient.set(key, JSON.stringify(pool)); + } finally { + // Unlock errors are ignored; the mutex lock expires via its TTL + await this.mutex.unlock(`lock:${key}`).catch(() => {}); } }, /** * Releases an acquired semaphore slot, marking it available again. - * Supports both callback and promise styles. * @param {string} key - The semaphore key. * @param {number} index - The slot index to release. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - releaseLock: (key, index, callback) => { - const executor = async () => { - // Mutex lock around semaphore - await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - - try { - const data = await getAsync(key); + releaseLock: async (key, index, ...rest) => { + assertNoCallback('pettyCache.semaphore.releaseLock', ...rest); - // If we don't have a previously created semaphore, return error - if (!data) { - throw new Error(`Semaphore ${key} doesn't exist.`); - } + // Mutex lock around semaphore + await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - const pool = JSON.parse(data); + try { + const data = await redisClient.get(key); - // Ensure index exists. - if (pool.length <= index) { - throw new Error(`Index ${index} for semaphore ${key} is invalid.`); - } + // If we don't have a previously created semaphore, return error + if (!data) { + throw new Error(`Semaphore ${key} doesn't exist.`); + } - pool[index] = { status: 'available' }; + const pool = JSON.parse(data); - await setAsync(key, JSON.stringify(pool)); - } finally { - // Unlock errors are ignored; the mutex lock expires via its TTL - await this.mutex.unlock(`lock:${key}`).catch(() => {}); + // Ensure index exists. + if (pool.length <= index) { + throw new Error(`Index ${index} for semaphore ${key} is invalid.`); } - }; - if (callback) { - deprecateCallback('pettyCache.semaphore.releaseLock'); - invokeCallback(executor(), callback); - } else { - return executor(); + pool[index] = { status: 'available' }; + + await redisClient.set(key, JSON.stringify(pool)); + } finally { + // Unlock errors are ignored; the mutex lock expires via its TTL + await this.mutex.unlock(`lock:${key}`).catch(() => {}); } }, /** * Resets all slots in an existing semaphore pool to available. - * Supports both callback and promise styles. * @param {string} key - The semaphore key. - * @param {Function} [callback] - Optional callback(err, pool). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the reset pool. + * @returns {Promise} Resolves with the reset pool. */ - reset: (key, callback) => { - const executor = async () => { - // Mutex lock around semaphore - await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); + reset: async (key, ...rest) => { + assertNoCallback('pettyCache.semaphore.reset', ...rest); - try { - // Try to get previously created semaphore - const data = await getAsync(key); + // Mutex lock around semaphore + await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - // If we don't have a previously created semaphore, return error - if (!data) { - throw new Error(`Semaphore ${key} doesn't exist.`); - } + try { + // Try to get previously created semaphore + const data = await redisClient.get(key); - let pool = JSON.parse(data); - pool = Array(pool.length).fill({ status: 'available' }); + // If we don't have a previously created semaphore, return error + if (!data) { + throw new Error(`Semaphore ${key} doesn't exist.`); + } - await setAsync(key, JSON.stringify(pool)); + let pool = JSON.parse(data); + pool = Array(pool.length).fill({ status: 'available' }); - return pool; - } finally { - // Unlock errors are ignored; the mutex lock expires via its TTL - await this.mutex.unlock(`lock:${key}`).catch(() => {}); - } - }; + await redisClient.set(key, JSON.stringify(pool)); - if (callback) { - deprecateCallback('pettyCache.semaphore.reset'); - invokeCallback(executor(), callback); - } else { - return executor(); + return pool; + } finally { + // Unlock errors are ignored; the mutex lock expires via its TTL + await this.mutex.unlock(`lock:${key}`).catch(() => {}); } }, /** * Retrieves an existing semaphore pool, or creates one if it doesn't exist. - * Supports both callback and promise styles. * @param {string} key - The semaphore key. * @param {Object} [options] - Optional settings. - * @param {number|Function} [options.size=1] - Pool size, or a function that resolves the size. Use size(callback) for callbacks or async size() for promises. - * @param {Function} [callback] - Optional callback(err, pool). If omitted, returns a Promise. - * @returns {Promise|undefined} Resolves with the semaphore pool. + * @param {number|Function} [options.size=1] - Pool size, or an async function that resolves the size. + * @returns {Promise} Resolves with the semaphore pool. */ - retrieveOrCreate: (key, options = {}, callback) => { - // Options are optional - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - const executor = async () => { - // Mutex lock around semaphore retrival or creation - await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); + retrieveOrCreate: async (key, options = {}, ...rest) => { + assertNoCallback('pettyCache.semaphore.retrieveOrCreate', options, ...rest); - try { - // Try to get previously created semaphore - const data = await getAsync(key); + // Mutex lock around semaphore retrival or creation + await this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }); - // If we retreived a previously created semaphore, return it - if (data) { - return JSON.parse(data); - } + try { + // Try to get previously created semaphore + const data = await redisClient.get(key); - let size; + // If we retreived a previously created semaphore, return it + if (data) { + return JSON.parse(data); + } - if (typeof options.size === 'function') { - size = await executeFunc(options.size); - } else { - size = Object.hasOwn(options, 'size') ? options.size : 1; - } + let size; - const pool = Array(Math.max(size, 1)).fill({ status: 'available' }); + if (typeof options.size === 'function') { + size = await options.size(); + } else { + size = Object.hasOwn(options, 'size') ? options.size : 1; + } - await setAsync(key, JSON.stringify(pool)); + const pool = Array(Math.max(size, 1)).fill({ status: 'available' }); - return pool; - } finally { - // Unlock errors are ignored; the mutex lock expires via its TTL - await this.mutex.unlock(`lock:${key}`).catch(() => {}); - } - }; + await redisClient.set(key, JSON.stringify(pool)); - if (callback) { - deprecateCallback('pettyCache.semaphore.retrieveOrCreate'); - invokeCallback(executor(), callback); - } else { - return executor(); + return pool; + } finally { + // Unlock errors are ignored; the mutex lock expires via its TTL + await this.mutex.unlock(`lock:${key}`).catch(() => {}); } } }; /** - * Stores a value in both the memory cache and Redis. Supports both callback and promise styles. + * Stores a value in both the memory cache and Redis. * @param {string} key - The cache key. * @param {*} value - The value to cache. * @param {Object} [options] - Optional settings. * @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties. - * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise. - * @returns {Promise|undefined} + * @returns {Promise} */ - this.set = (key, value, options = {}, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; - } + this.set = async (key, value, options = {}, ...rest) => { + assertNoCallback('pettyCache.set', options, ...rest); // Get TTL based on specified options const ttl = getTtl(options); - const executor = async () => { - // Store value in memory cache with a short expiration - memoryCache.put(key, value, random(2000, 5000)); - - // Store value in Redis - await psetexAsync(key, random(ttl.min, ttl.max), PettyCache.stringify(value)); - }; + // Store value in memory cache with a short expiration + memoryCache.put(key, value, random(2000, 5000)); - if (callback) { - deprecateCallback('pettyCache.set'); - invokeCallback(executor(), callback); - } else { - return executor(); - } + // Store value in Redis + await redisClient.pSetEx(key, random(ttl.min, ttl.max), PettyCache.stringify(value)); }; } diff --git a/package.json b/package.json index 4789542..61ca064 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "dependencies": { "lock": "~1.1.0", "memory-cache": "~0.2.0", - "redis": "~3.1.0" + "redis": "~6.2.1" }, "description": "A cache module for node.js that uses a two-level cache (in-memory cache for recently accessed data plus Redis for distributed caching) with some extra features to avoid cache stampedes and thundering herds.", "devDependencies": { @@ -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": "4.0.1" + "version": "5.0.0" } diff --git a/test/index.js b/test/index.js index 5bad200..05250fe 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,5 @@ const test = require('node:test'); const assert = require('node:assert'); -const childProcess = require('node:child_process'); const timers = require('node:timers/promises'); const memoryCache = require('memory-cache'); @@ -11,91 +10,73 @@ const PettyCache = require('../index.js'); const redisClient = redis.createClient(); const pettyCache = new PettyCache(redisClient); -// Collect deprecation warnings emitted while the suite exercises callback-style APIs -const deprecationWarnings = []; - -process.on('warning', (warning) => { - if (warning.name === 'DeprecationWarning') { - deprecationWarnings.push(warning.message); - } -}); - test('petty-cache', { concurrency: true }, async (t) => { t.test('new PettyCache()', { concurrency: true }, async (t) => { - t.test('new PettyCache()', (t, done) => { + t.test('new PettyCache()', async () => { const key = Math.random().toString(); const newPettyCache = new PettyCache(); - newPettyCache.fetch(key, (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + const data = await newPettyCache.fetch(key, async () => ({ foo: 'bar' })); + + assert.equal(data.foo, 'bar'); + + const cached = await newPettyCache.fetch(key, () => { + throw 'This function should not be called'; }); - }); - t.test('new PettyCache(port, host)', (t, done) => { - const key = Math.random().toString(); - const newPettyCache = new PettyCache(6379, 'localhost'); - - newPettyCache.fetch(key, (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + assert.equal(cached.foo, 'bar'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await newPettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis.foo, 'bar'); + }); + + t.test('new PettyCache(options) should pass options straight to node-redis', () => { + const originalCreateClient = redis.createClient; + let capturedOptions; + + redis.createClient = (options) => { + capturedOptions = options; + return originalCreateClient(); + }; + + const newPettyCache = new PettyCache({ database: 2, password: 'secret', socket: { host: 'localhost', port: 6379 } }); + + redis.createClient = originalCreateClient; + + assert(newPettyCache); + assert.deepStrictEqual(capturedOptions, { database: 2, password: 'secret', socket: { host: 'localhost', port: 6379 } }); }); - t.test('new PettyCache(redisClient)', (t, done) => { + + t.test('new PettyCache(redisClient)', async () => { const key = Math.random().toString(); const redisClient = redis.createClient(); const newPettyCache = new PettyCache(redisClient); - newPettyCache.fetch(key, (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - newPettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + const data = await newPettyCache.fetch(key, async () => ({ foo: 'bar' })); + + assert.equal(data.foo, 'bar'); + + const cached = await newPettyCache.fetch(key, () => { + throw 'This function should not be called'; + }); + + assert.equal(cached.foo, 'bar'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await newPettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis.foo, 'bar'); }); }); @@ -192,7 +173,7 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.bulkFetch', { concurrency: true }, async (t) => { - t.test('PettyCache.bulkFetch', (t, done) => { + t.test('PettyCache.bulkFetch', async () => { // Use per-run keys so values left in Redis by a previous test run can't expire mid-test const prefix = Math.random().toString(); const keyA = `${prefix}-a`; @@ -200,120 +181,97 @@ test('petty-cache', { concurrency: true }, async (t) => { const keyC = `${prefix}-c`; const keyD = `${prefix}-d`; - pettyCache.set(keyA, 1, () => { - pettyCache.set(keyB, '2', () => { - pettyCache.bulkFetch([keyA, keyB, keyC, keyD], (keys, callback) => { - assert(keys.length === 2); - - const data = {}; - - data[keyC] = [3]; - data[keyD] = { num: 4 }; - - callback(null, data); - }, (err, values) => { - assert.strictEqual(values[keyA], 1); - assert.strictEqual(values[keyB], '2'); - assert.strictEqual(values[keyC][0], 3); - assert.strictEqual(values[keyD].num, 4); - - // Call bulkFetch again to ensure memory serialization is working as expected. - pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { - throw 'This function should not be called'; - }, (err, values) => { - assert.strictEqual(values[keyA], 1); - assert.strictEqual(values[keyB], '2'); - assert.strictEqual(values[keyC][0], 3); - assert.strictEqual(values[keyD].num, 4); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { - throw 'This function should not be called'; - }, (err, values) => { - assert.strictEqual(values[keyA], 1); - assert.strictEqual(values[keyB], '2'); - assert.strictEqual(values[keyC][0], 3); - assert.strictEqual(values[keyD].num, 4); - - // Call bulkFetch again to ensure memory serialization is working as expected. - pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { - throw 'This function should not be called'; - }, (err, values) => { - assert.strictEqual(values[keyA], 1); - assert.strictEqual(values[keyB], '2'); - assert.strictEqual(values[keyC][0], 3); - assert.strictEqual(values[keyD].num, 4); - done(); - }); - }); - }, 5001); - }); - }); - }); + await pettyCache.set(keyA, 1); + await pettyCache.set(keyB, '2'); + + const values = await pettyCache.bulkFetch([keyA, keyB, keyC, keyD], async (keys) => { + assert(keys.length === 2); + + const data = {}; + + data[keyC] = [3]; + data[keyD] = { num: 4 }; + + return data; + }); + + assert.strictEqual(values[keyA], 1); + assert.strictEqual(values[keyB], '2'); + assert.strictEqual(values[keyC][0], 3); + assert.strictEqual(values[keyD].num, 4); + + // Call bulkFetch again to ensure memory serialization is working as expected. + const fromMemory = await pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { + throw 'This function should not be called'; + }); + + assert.strictEqual(fromMemory[keyA], 1); + assert.strictEqual(fromMemory[keyB], '2'); + assert.strictEqual(fromMemory[keyC][0], 3); + assert.strictEqual(fromMemory[keyD].num, 4); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { + throw 'This function should not be called'; + }); + + assert.strictEqual(fromRedis[keyA], 1); + assert.strictEqual(fromRedis[keyB], '2'); + assert.strictEqual(fromRedis[keyC][0], 3); + assert.strictEqual(fromRedis[keyD].num, 4); + + // Call bulkFetch again to ensure memory serialization is working as expected. + const fromMemoryAgain = await pettyCache.bulkFetch([keyA, keyB, keyC, keyD], () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromMemoryAgain[keyA], 1); + assert.strictEqual(fromMemoryAgain[keyB], '2'); + assert.strictEqual(fromMemoryAgain[keyC][0], 3); + assert.strictEqual(fromMemoryAgain[keyD].num, 4); }); - t.test('PettyCache.bulkFetch should cache null values returned by func', (t, done) => { + t.test('PettyCache.bulkFetch should cache null values returned by func', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); - pettyCache.bulkFetch([key1, key2], (keys, callback) => { + const values = await pettyCache.bulkFetch([key1, key2], async (keys) => { assert.strictEqual(keys.length, 2); assert(keys.some(k => k === key1)); assert(keys.some(k => k === key2)); - const values = {}; - - values[key1] = '1'; - values[key2] = null; - - callback(null, values); - }, (err) => { - assert.ifError(err); - - pettyCache.bulkFetch([key1, key2], () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(Object.keys(data).length, 2); - assert.strictEqual(data[key1], '1'); - assert.strictEqual(data[key2], null); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.bulkFetch([key1, key2], () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(Object.keys(data).length, 2); - assert.strictEqual(data[key1], '1'); - assert.strictEqual(data[key2], null); - - done(); - }); - }, 5001); - }); + const data = {}; + + data[key1] = '1'; + data[key2] = null; + + return data; }); - }); - t.test('PettyCache.bulkFetch should return empty object when no keys are passed', (t, done) => { - pettyCache.bulkFetch([], () => { + assert.strictEqual(Object.keys(values).length, 2); + assert.strictEqual(values[key1], '1'); + assert.strictEqual(values[key2], null); + + const fromMemory = await pettyCache.bulkFetch([key1, key2], () => { throw 'This function should not be called'; - }, (err, values) => { - assert.ifError(err); - assert.deepEqual(values, {}); - done(); }); - }); - t.test('PettyCache.bulkFetch should return error if func returns error', (t, done) => { - pettyCache.bulkFetch([Math.random().toString()], (keys, callback) => { - callback(new Error('PettyCache.bulkFetch should return error if func returns error')); - }, (err, values) => { - assert(err); - assert.strictEqual(err.message, 'PettyCache.bulkFetch should return error if func returns error'); - assert(!values); - done(); + assert.strictEqual(Object.keys(fromMemory).length, 2); + assert.strictEqual(fromMemory[key1], '1'); + assert.strictEqual(fromMemory[key2], null); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.bulkFetch([key1, key2], () => { + throw 'This function should not be called'; }); + + assert.strictEqual(Object.keys(fromRedis).length, 2); + assert.strictEqual(fromRedis[key1], '1'); + assert.strictEqual(fromRedis[key2], null); }); t.test('PettyCache.bulkFetch should return values (promises)', async () => { @@ -322,10 +280,10 @@ test('petty-cache', { concurrency: true }, async (t) => { await pettyCache.set(key1, '1'); - const values = await pettyCache.bulkFetch([key1, key2], (keys, callback) => { + const values = await pettyCache.bulkFetch([key1, key2], async () => { const data = {}; data[key2] = '2'; - callback(null, data); + return data; }); assert.strictEqual(values[key1], '1'); @@ -339,21 +297,12 @@ test('petty-cache', { concurrency: true }, async (t) => { assert.deepEqual(values, {}); }); - t.test('PettyCache.bulkFetch should reject if func returns error (promises)', async () => { - await assert.rejects( - pettyCache.bulkFetch([Math.random().toString()], (keys, callback) => { - callback(new Error('PettyCache.bulkFetch should reject if func returns error')); - }), - { message: 'PettyCache.bulkFetch should reject if func returns error' } - ); - }); - t.test('PettyCache.bulkFetch should return values with options (promises)', async () => { const key = Math.random().toString(); - const values = await pettyCache.bulkFetch([key], (keys, callback) => { + const values = await pettyCache.bulkFetch([key], async (keys) => { const result = {}; keys.forEach(k => { result[k] = 'value'; }); - callback(null, result); + return result; }, { ttl: 6000 }); assert.deepEqual(values, { [key]: 'value' }); }); @@ -383,133 +332,123 @@ test('petty-cache', { concurrency: true }, async (t) => { ); }); - t.test('PettyCache.bulkFetch should run func again after TTL', (t, done) => { + t.test('PettyCache.bulkFetch should run func again after TTL', async () => { const keys = [Math.random().toString(), Math.random().toString()]; let numberOfFuncCalls = 0; - const func = (keys, callback) => { + const func = async (keys) => { numberOfFuncCalls++; const results = {}; results[keys[0]] = numberOfFuncCalls; results[keys[1]] = numberOfFuncCalls; - callback(null, results); + return results; }; - pettyCache.bulkFetch(keys, func, { ttl: 6000 }, (err, results) => { - assert.ifError(err); - assert.strictEqual(results[keys[0]], 1); - assert.strictEqual(results[keys[1]], 1); - - pettyCache.bulkGet(keys, (err, results) => { - assert.ifError(err); - assert.strictEqual(results[keys[0]], 1); - assert.strictEqual(results[keys[1]], 1); - }); - - setTimeout(() => { - pettyCache.bulkGet(keys, (err, results) => { - assert.ifError(err); - assert.strictEqual(results[keys[0]], null); - assert.strictEqual(results[keys[1]], null); - - pettyCache.bulkFetch(keys, func, { ttl: 6000 }, (err, results) => { - assert.ifError(err); - assert.strictEqual(results[keys[0]], 2); - assert.strictEqual(results[keys[1]], 2); - - pettyCache.bulkGet(keys, (err, results) => { - assert.ifError(err); - assert.strictEqual(results[keys[0]], 2); - assert.strictEqual(results[keys[1]], 2); - done(); - }); - }); - }); - }, 6001); - }); + const results = await pettyCache.bulkFetch(keys, func, { ttl: 6000 }); + + assert.strictEqual(results[keys[0]], 1); + assert.strictEqual(results[keys[1]], 1); + + const cached = await pettyCache.bulkGet(keys); + + assert.strictEqual(cached[keys[0]], 1); + assert.strictEqual(cached[keys[1]], 1); + + // Wait for the TTL to expire + await timers.setTimeout(6001); + + const expired = await pettyCache.bulkGet(keys); + + assert.strictEqual(expired[keys[0]], null); + assert.strictEqual(expired[keys[1]], null); + + const refetched = await pettyCache.bulkFetch(keys, func, { ttl: 6000 }); + + assert.strictEqual(refetched[keys[0]], 2); + assert.strictEqual(refetched[keys[1]], 2); + + const recached = await pettyCache.bulkGet(keys); + + assert.strictEqual(recached[keys[0]], 2); + assert.strictEqual(recached[keys[1]], 2); }); }); t.test('PettyCache.bulkGet', { concurrency: true }, async (t) => { - t.test('PettyCache.bulkGet should return values', (t, done) => { + t.test('PettyCache.bulkGet should return values', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); - pettyCache.set(key1, '1', () => { - pettyCache.set(key2, '2', () => { - pettyCache.set(key3, '3', () => { - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], '3'); - - // Call bulkGet again while values are still in memory cache - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], '3'); - - // Wait for memory cache to expire - setTimeout(() => { - // Ensure keys are still in Redis - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], '3'); - done(); - }); - }, 5001); - }); - }); - }); - }); - }); + await pettyCache.set(key1, '1'); + await pettyCache.set(key2, '2'); + await pettyCache.set(key3, '3'); + + const values = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(values).length, 3); + assert.strictEqual(values[key1], '1'); + assert.strictEqual(values[key2], '2'); + assert.strictEqual(values[key3], '3'); + + // Call bulkGet again while values are still in memory cache + const fromMemory = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(fromMemory).length, 3); + assert.strictEqual(fromMemory[key1], '1'); + assert.strictEqual(fromMemory[key2], '2'); + assert.strictEqual(fromMemory[key3], '3'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + // Ensure keys are still in Redis + const fromRedis = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(fromRedis).length, 3); + assert.strictEqual(fromRedis[key1], '1'); + assert.strictEqual(fromRedis[key2], '2'); + assert.strictEqual(fromRedis[key3], '3'); }); - t.test('PettyCache.bulkGet should return null for missing keys', (t, done) => { + t.test('PettyCache.bulkGet should return null for missing keys', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); - pettyCache.set(key1, '1', () => { - pettyCache.set(key2, '2', () => { - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], null); - - // Call bulkGet again while values are still in memory cache - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], null); - - // Wait for memory cache to expire - setTimeout(() => { - // Ensure keys are still in Redis - pettyCache.bulkGet([key1, key2, key3], (err, values) => { - assert.strictEqual(Object.keys(values).length, 3); - assert.strictEqual(values[key1], '1'); - assert.strictEqual(values[key2], '2'); - assert.strictEqual(values[key3], null); - done(); - }); - }, 5001); - }); - }); - }); - }); + await pettyCache.set(key1, '1'); + await pettyCache.set(key2, '2'); + + const values = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(values).length, 3); + assert.strictEqual(values[key1], '1'); + assert.strictEqual(values[key2], '2'); + assert.strictEqual(values[key3], null); + + // Call bulkGet again while values are still in memory cache + const fromMemory = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(fromMemory).length, 3); + assert.strictEqual(fromMemory[key1], '1'); + assert.strictEqual(fromMemory[key2], '2'); + assert.strictEqual(fromMemory[key3], null); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + // Ensure keys are still in Redis + const fromRedis = await pettyCache.bulkGet([key1, key2, key3]); + + assert.strictEqual(Object.keys(fromRedis).length, 3); + assert.strictEqual(fromRedis[key1], '1'); + assert.strictEqual(fromRedis[key2], '2'); + assert.strictEqual(fromRedis[key3], null); }); - t.test('PettyCache.bulkGet should correctly handle falsy values', (t, done) => { + t.test('PettyCache.bulkGet should correctly handle falsy values', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -525,69 +464,57 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key5] = null; values[key6] = undefined; - Promise.all(Object.keys(values).map(key => pettyCache.set(key, values[key], { ttl: 6000 }))).then(() => { - const keys = Object.keys(values); - - // Add an additional key to check handling of missing keys - const key7 = Math.random().toString(); - keys.push(key7); - - pettyCache.bulkGet(keys, (err, data) => { - assert.ifError(err); - assert.strictEqual(keys.length, 7); - assert.strictEqual(Object.keys(data).length, 7); - assert.strictEqual(data[key1], ''); - assert.strictEqual(data[key2], 0); - assert.strictEqual(data[key3], false); - assert.strictEqual(typeof data[key4], 'number'); - assert(isNaN(data[key4])); - assert.strictEqual(data[key5], null); - assert.strictEqual(data[key6], undefined); - assert.strictEqual(data[key7], null); - - // Wait for memory cache to expire - setTimeout(() => { - // Ensure keys are still in Redis - pettyCache.bulkGet(keys, (err, data) => { - assert.ifError(err); - assert.strictEqual(Object.keys(data).length, 7); - assert.strictEqual(data[key1], ''); - assert.strictEqual(data[key2], 0); - assert.strictEqual(data[key3], false); - assert.strictEqual(typeof data[key4], 'number'); - assert(isNaN(data[key4])); - assert.strictEqual(data[key5], null); - assert.strictEqual(data[key6], undefined); - assert.strictEqual(data[key7], null); - - // Wait for Redis cache to expire - setTimeout(() => { - // Ensure keys are not in Redis - pettyCache.bulkGet(keys, (err, data) => { - assert.ifError(err); - assert.strictEqual(Object.keys(data).length, 7); - assert.strictEqual(data[key1], null); - assert.strictEqual(data[key2], null); - assert.strictEqual(data[key3], null); - assert.strictEqual(data[key4], null); - assert.strictEqual(data[key5], null); - assert.strictEqual(data[key6], null); - assert.strictEqual(data[key7], null); - done(); - }); - }, 6001); - }); - }, 5001); - }); - }); - }); + await Promise.all(Object.keys(values).map(key => pettyCache.set(key, values[key], { ttl: 6000 }))); - t.test('PettyCache.bulkGet should return empty object when no keys are passed', (t, done) => { - pettyCache.bulkGet([], (err, values) => { - assert.ifError(err); - assert.deepEqual(values, {}); - done(); - }); + const keys = Object.keys(values); + + // Add an additional key to check handling of missing keys + const key7 = Math.random().toString(); + keys.push(key7); + + const data = await pettyCache.bulkGet(keys); + + assert.strictEqual(keys.length, 7); + assert.strictEqual(Object.keys(data).length, 7); + assert.strictEqual(data[key1], ''); + assert.strictEqual(data[key2], 0); + assert.strictEqual(data[key3], false); + assert.strictEqual(typeof data[key4], 'number'); + assert(isNaN(data[key4])); + assert.strictEqual(data[key5], null); + assert.strictEqual(data[key6], undefined); + assert.strictEqual(data[key7], null); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + // Ensure keys are still in Redis + const fromRedis = await pettyCache.bulkGet(keys); + + assert.strictEqual(Object.keys(fromRedis).length, 7); + assert.strictEqual(fromRedis[key1], ''); + assert.strictEqual(fromRedis[key2], 0); + assert.strictEqual(fromRedis[key3], false); + assert.strictEqual(typeof fromRedis[key4], 'number'); + assert(isNaN(fromRedis[key4])); + assert.strictEqual(fromRedis[key5], null); + assert.strictEqual(fromRedis[key6], undefined); + assert.strictEqual(fromRedis[key7], null); + + // Wait for Redis cache to expire + await timers.setTimeout(6001); + + // Ensure keys are not in Redis + const expired = await pettyCache.bulkGet(keys); + + assert.strictEqual(Object.keys(expired).length, 7); + assert.strictEqual(expired[key1], null); + assert.strictEqual(expired[key2], null); + assert.strictEqual(expired[key3], null); + assert.strictEqual(expired[key4], null); + assert.strictEqual(expired[key5], null); + assert.strictEqual(expired[key6], null); + assert.strictEqual(expired[key7], null); }); t.test('PettyCache.bulkGet should return values (promises)', async () => { @@ -620,7 +547,7 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.bulkSet', { concurrency: true }, async (t) => { - t.test('PettyCache.bulkSet should set values', (t, done) => { + t.test('PettyCache.bulkSet should set values', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -630,46 +557,21 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key2] = 2; values[key3] = '3'; - pettyCache.bulkSet(values, (err) => { - assert.ifError(err); - - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 2); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '3'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 2); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '3'); - done(); - }); - }); - }); - }, 5001); - }); - }); - }); - }); + await pettyCache.bulkSet(values); + + assert.strictEqual(await pettyCache.get(key1), '1'); + assert.strictEqual(await pettyCache.get(key2), 2); + assert.strictEqual(await pettyCache.get(key3), '3'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key1), '1'); + assert.strictEqual(await pettyCache.get(key2), 2); + assert.strictEqual(await pettyCache.get(key3), '3'); }); - t.test('PettyCache.bulkSet should set values with the specified TTL option', (t, done) => { + t.test('PettyCache.bulkSet should set values with the specified TTL option', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -679,46 +581,21 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key2] = 2; values[key3] = '3'; - pettyCache.bulkSet(values, { ttl: 6000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 2); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '3'); - - // Wait for Redis cache to expire - setTimeout(() => { - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }); - }); - }, 6001); - }); - }); - }); - }); + await pettyCache.bulkSet(values, { ttl: 6000 }); + + assert.strictEqual(await pettyCache.get(key1), '1'); + assert.strictEqual(await pettyCache.get(key2), 2); + assert.strictEqual(await pettyCache.get(key3), '3'); + + // Wait for Redis cache to expire + await timers.setTimeout(6001); + + assert.strictEqual(await pettyCache.get(key1), null); + assert.strictEqual(await pettyCache.get(key2), null); + assert.strictEqual(await pettyCache.get(key3), null); }); - t.test('PettyCache.bulkSet should set values with the specified TTL option using max and min', (t, done) => { + t.test('PettyCache.bulkSet should set values with the specified TTL option using max and min', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -728,46 +605,21 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key2] = 2; values[key3] = '3'; - pettyCache.bulkSet(values, { ttl: { max: 7000, min: 6000 } }, (err) => { - assert.ifError(err); - - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 2); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '3'); - - // Wait for Redis cache to expire - setTimeout(() => { - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - pettyCache.get(key2, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - pettyCache.get(key3, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }); - }); - }, 7001); - }); - }); - }); - }); + await pettyCache.bulkSet(values, { ttl: { max: 7000, min: 6000 } }); + + assert.strictEqual(await pettyCache.get(key1), '1'); + assert.strictEqual(await pettyCache.get(key2), 2); + assert.strictEqual(await pettyCache.get(key3), '3'); + + // Wait for Redis cache to expire + await timers.setTimeout(7001); + + assert.strictEqual(await pettyCache.get(key1), null); + assert.strictEqual(await pettyCache.get(key2), null); + assert.strictEqual(await pettyCache.get(key3), null); }); - t.test('PettyCache.bulkSet should set values with the specified TTL option using max only', (t, done) => { + t.test('PettyCache.bulkSet should set values with the specified TTL option using max only', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -777,19 +629,12 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key2] = 2; values[key3] = '3'; - pettyCache.bulkSet(values, { ttl: { max: 10000 } }, (err) => { - assert.ifError(err); - - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); + await pettyCache.bulkSet(values, { ttl: { max: 10000 } }); - done(); - }); - }); + assert.strictEqual(await pettyCache.get(key1), '1'); }); - t.test('PettyCache.bulkSet should set values with the specified TTL option using min only', (t, done) => { + t.test('PettyCache.bulkSet should set values with the specified TTL option using min only', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -799,16 +644,9 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key2] = 2; values[key3] = '3'; - pettyCache.bulkSet(values, { ttl: { min: 6000 } }, (err) => { - assert.ifError(err); + await pettyCache.bulkSet(values, { ttl: { min: 6000 } }); - pettyCache.get(key1, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, '1'); - - done(); - }); - }); + assert.strictEqual(await pettyCache.get(key1), '1'); }); t.test('PettyCache.bulkSet should set values (promises)', async () => { @@ -838,53 +676,19 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.del', { concurrency: true }, async (t) => { - t.test('PettyCache.del', (t, done) => { - const key = Math.random().toString(); - - pettyCache.set(key, key.split('').reverse().join(''), (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, key.split('').reverse().join('')); - - pettyCache.del(key, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - pettyCache.del(key, (err) => { - assert.ifError(err); - done(); - }); - }); - }); - }); - }); - }); - - t.test('PettyCache.del', (t, done) => { + t.test('PettyCache.del', async () => { const key = Math.random().toString(); - pettyCache.set(key, key.split('').reverse().join(''), (err) => { - assert.ifError(err); + await pettyCache.set(key, key.split('').reverse().join('')); - pettyCache.get(key, async (err, value) => { - assert.strictEqual(value, key.split('').reverse().join('')); + assert.strictEqual(await pettyCache.get(key), key.split('').reverse().join('')); - await pettyCache.del(key); - - pettyCache.get(key, async (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); + await pettyCache.del(key); - await pettyCache.del(key); + assert.strictEqual(await pettyCache.get(key), null); - done(); - }); - }); - }); + // Deleting a key that no longer exists should not error + await pettyCache.del(key); }); t.test('PettyCache.del (promises)', async () => { @@ -899,238 +703,143 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.fetch', { concurrency: true }, async (t) => { - t.test('PettyCache.fetch', (t, done) => { + t.test('PettyCache.fetch', async () => { const key = Math.random().toString(); - pettyCache.fetch(key, (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + const data = await pettyCache.fetch(key, async () => ({ foo: 'bar' })); + + assert.equal(data.foo, 'bar'); + + const fromMemory = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; + }); + + assert.equal(fromMemory.foo, 'bar'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis.foo, 'bar'); }); - t.test('PettyCache.fetch should cache null values returned by func', (t, done) => { + t.test('PettyCache.fetch should cache null values returned by func', async () => { const key = Math.random().toString(); - pettyCache.fetch(key, (callback) => { - return callback(null, null); - }, () => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data, null); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data, null); - done(); - }); - }, 5001); - }); + const data = await pettyCache.fetch(key, async () => null); + + assert.strictEqual(data, null); + + const fromMemory = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; + }); + + assert.strictEqual(fromMemory, null); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis, null); }); - t.test('PettyCache.fetch should cache undefined values returned by func', (t, done) => { + t.test('PettyCache.fetch should cache undefined values returned by func', async () => { const key = Math.random().toString(); - pettyCache.fetch(key, (callback) => { - return callback(null, undefined); - }, () => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data, undefined); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data, undefined); - done(); - }); - }, 5001); - }); + const data = await pettyCache.fetch(key, async () => undefined); + + assert.strictEqual(data, undefined); + + const fromMemory = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; + }); + + assert.strictEqual(fromMemory, undefined); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis, undefined); }); - t.test('PettyCache.fetch should lock around func', (t, done) => { + t.test('PettyCache.fetch should lock around func', async () => { const key = Math.random().toString(); let numberOfFuncCalls = 0; - const func = (callback) => { - setTimeout(() => { - callback(null, ++numberOfFuncCalls); - }, 100); + const func = async () => { + await timers.setTimeout(100); + return ++numberOfFuncCalls; }; - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - pettyCache.fetch(key, func, () => {}); - - pettyCache.fetch(key, func, (err, data) => { - assert.equal(data, 1); - done(); - }); + const results = await Promise.all(Array.from({ length: 10 }, () => pettyCache.fetch(key, func))); + + results.forEach(data => assert.equal(data, 1)); }); - t.test('PettyCache.fetch should run func again after TTL', (t, done) => { + t.test('PettyCache.fetch should run func again after TTL', async () => { const key = Math.random().toString(); let numberOfFuncCalls = 0; - const func = (callback) => { - setTimeout(() => { - callback(null, ++numberOfFuncCalls); - }, 100); + const func = async () => { + await timers.setTimeout(100); + return ++numberOfFuncCalls; }; - pettyCache.fetch(key, func, { ttl: 6000 }, () => {}); + const data = await pettyCache.fetch(key, func, { ttl: 6000 }); - pettyCache.fetch(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 1); + assert.equal(data, 1); - setTimeout(() => { - pettyCache.fetch(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 2); + // Wait for the TTL to expire + await timers.setTimeout(6001); - pettyCache.fetch(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 2); - done(); - }); - }); - }, 6001); - }); - }); + const refetched = await pettyCache.fetch(key, func, { ttl: 6000 }); - t.test('PettyCache.fetch should return error if func returns error', (t, done) => { - pettyCache.fetch(Math.random().toString(), (callback) => { - callback(new Error('PettyCache.fetch should return error if func returns error')); - }, (err, values) => { - assert(err); - assert.strictEqual(err.message, 'PettyCache.fetch should return error if func returns error'); - assert(!values); - done(); - }); + assert.equal(refetched, 2); + + const cached = await pettyCache.fetch(key, func, { ttl: 6000 }); + + assert.equal(cached, 2); }); - t.test('PettyCache.fetch should support async func', (t, done) => { + t.test('PettyCache.fetch should support sync func without callback', async () => { const key = Math.random().toString(); - pettyCache.fetch(key, async () => { + const data = await pettyCache.fetch(key, () => { return { foo: 'bar' }; - }, () => { - pettyCache.fetch(key, async () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, async () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); }); - }); - t.test('PettyCache.fetch should return error if async func throws error', (t, done) => { - pettyCache.fetch(Math.random().toString(), async () => { - throw new Error('PettyCache.fetch should return error if async func throws error'); - }, (err, data) => { - assert(err); - assert.strictEqual(err.message, 'PettyCache.fetch should return error if async func throws error'); - assert(!data); - done(); - }); - }); + assert.equal(data.foo, 'bar'); - t.test('PettyCache.fetch should support async func with callback', (t, done) => { - const key = Math.random().toString(); - - pettyCache.fetch(key, async (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - pettyCache.fetch(key, async () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, async () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + const fromMemory = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; }); - }); - t.test('PettyCache.fetch should support sync func without callback', (t, done) => { - const key = Math.random().toString(); + assert.equal(fromMemory.foo, 'bar'); - pettyCache.fetch(key, () => { - return { foo: 'bar' }; - }, () => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetch(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.fetch(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis.foo, 'bar'); }); t.test('PettyCache.fetch should return value (promises)', async () => { const key = Math.random().toString(); - const data = await pettyCache.fetch(key, (callback) => { - return callback(null, { foo: 'bar' }); - }); + const data = await pettyCache.fetch(key, async () => ({ foo: 'bar' })); assert.strictEqual(data.foo, 'bar'); @@ -1141,15 +850,6 @@ test('petty-cache', { concurrency: true }, async (t) => { assert.strictEqual(cached.foo, 'bar'); }); - t.test('PettyCache.fetch should reject if func returns error (promises)', async () => { - await assert.rejects( - pettyCache.fetch(Math.random().toString(), (callback) => { - callback(new Error('PettyCache.fetch should reject if func returns error')); - }), - { message: 'PettyCache.fetch should reject if func returns error' } - ); - }); - t.test('PettyCache.fetch should support async func (promises)', async () => { const key = Math.random().toString(); @@ -1172,133 +872,117 @@ test('petty-cache', { concurrency: true }, async (t) => { t.test('PettyCache.fetch should return value with options (promises)', async () => { const key = Math.random().toString(); - const data = await pettyCache.fetch(key, (callback) => { - return callback(null, 'value'); - }, { ttl: 6000 }); + const data = await pettyCache.fetch(key, async () => 'value', { ttl: 6000 }); assert.strictEqual(data, 'value'); }); }); t.test('PettyCache.fetchAndRefresh', { concurrency: true }, async (t) => { - t.test('PettyCache.fetchAndRefresh', (t, done) => { + t.test('PettyCache.fetchAndRefresh', async () => { const key = Math.random().toString(); - pettyCache.fetchAndRefresh(key, (callback) => { - return callback(null, { foo: 'bar' }); - }, () => { - pettyCache.fetchAndRefresh(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.equal(data.foo, 'bar'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.fetchAndRefresh(key, () => { - throw 'This function should not be called'; - }, (err, data) => { - assert.strictEqual(data.foo, 'bar'); - done(); - }); - }, 5001); - }); + const data = await pettyCache.fetchAndRefresh(key, async () => ({ foo: 'bar' })); + + assert.equal(data.foo, 'bar'); + + const fromMemory = await pettyCache.fetchAndRefresh(key, () => { + throw 'This function should not be called'; + }); + + assert.equal(fromMemory.foo, 'bar'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + const fromRedis = await pettyCache.fetchAndRefresh(key, () => { + throw 'This function should not be called'; }); + + assert.strictEqual(fromRedis.foo, 'bar'); }); - t.test('PettyCache.fetchAndRefresh should run func again to refresh', (t, done) => { + t.test('PettyCache.fetchAndRefresh should run func again to refresh', async () => { const key = Math.random().toString(); let numberOfFuncCalls = 0; - const func = (callback) => { - setTimeout(() => { - callback(null, ++numberOfFuncCalls); - }, 100); + const func = async () => { + await timers.setTimeout(100); + return ++numberOfFuncCalls; }; - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); + const data = await pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 1); + assert.equal(data, 1); - setTimeout(() => { - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 2); + // Wait for the background refresh interval (ttl.min / 2) + await timers.setTimeout(3001); - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.equal(data, 2); - done(); - }); - }); - }, 3001); - }); + const refreshed = await pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(refreshed, 2); + + const cached = await pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(cached, 2); }); - t.test('PettyCache.fetchAndRefresh should not allow multiple clients to execute func at the same time', (t, done) => { + t.test('PettyCache.fetchAndRefresh should not allow multiple clients to execute func at the same time', async () => { const key = Math.random().toString(); let numberOfFuncCalls = 0; - const func = (callback) => { - setTimeout(() => { - callback(null, ++numberOfFuncCalls); - }, 100); + const func = async () => { + await timers.setTimeout(100); + return ++numberOfFuncCalls; }; - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.ifError(err); - assert.equal(data, 1); - - const pettyCache2 = new PettyCache(redisClient); - - pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.ifError(err); - assert.equal(data, 1); - - setTimeout(() => { - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.ifError(err); - assert.equal(data, 2); - - pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert.ifError(err); - assert.equal(data, 2); - done(); - }); - }); - }, 5001); - }); - }); + const data = await pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(data, 1); + + const pettyCache2 = new PettyCache(redisClient); + + const data2 = await pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(data2, 1); + + // Wait for the background refresh; the interval mutex should allow only one client to refresh + await timers.setTimeout(5001); + + const refreshed = await pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(refreshed, 2); + + const refreshed2 = await pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }); + + assert.equal(refreshed2, 2); }); - t.test('PettyCache.fetchAndRefresh should return error if func returns error', (t, done) => { + t.test('PettyCache.fetchAndRefresh should reject if func throws error', async () => { const key = Math.random().toString(); - const func = (callback) => { - callback(new Error('PettyCache.fetchAndRefresh should return error if func returns error')); + const func = async () => { + throw new Error('PettyCache.fetchAndRefresh should reject if func throws error'); }; - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert(err); - assert.strictEqual(err.message, 'PettyCache.fetchAndRefresh should return error if func returns error'); - assert(!data); + await assert.rejects( + pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }), + { message: 'PettyCache.fetchAndRefresh should reject if func throws error' } + ); - setTimeout(() => { - pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, (err, data) => { - assert(err); - assert.strictEqual(err.message, 'PettyCache.fetchAndRefresh should return error if func returns error'); - assert(!data); + // Wait past a refresh interval; failed refreshes are swallowed and fetch fails again + await timers.setTimeout(3001); - done(); - }); - }, 3001); - }); + await assert.rejects( + pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }), + { message: 'PettyCache.fetchAndRefresh should reject if func throws error' } + ); }); - t.test('PettyCache.fetchAndRefresh should not require options', (t, done) => { - pettyCache.fetchAndRefresh(Math.random().toString(), (callback) => { - return callback(null, { foo: 'bar' }); - }); + t.test('PettyCache.fetchAndRefresh should not require options', async () => { + const data = await pettyCache.fetchAndRefresh(Math.random().toString(), async () => ({ foo: 'bar' })); - done(); + assert.equal(data.foo, 'bar'); }); t.test('PettyCache.fetchAndRefresh should support async func and refresh it (promises)', async () => { @@ -1318,46 +1002,27 @@ test('petty-cache', { concurrency: true }, async (t) => { assert.ok(await pettyCache.get(key) >= 2, 'the refreshed value should have been stored in cache'); }); - t.test('PettyCache.fetchAndRefresh should reject if func returns error (promises)', async () => { - await assert.rejects( - pettyCache.fetchAndRefresh(Math.random().toString(), (callback) => { - callback(new Error('PettyCache.fetchAndRefresh should reject if func returns error')); - }, { ttl: 6000 }), - { message: 'PettyCache.fetchAndRefresh should reject if func returns error' } - ); - }); }); t.test('PettyCache.get', { concurrency: true }, async (t) => { - t.test('PettyCache.get should return value', (t, done) => { + t.test('PettyCache.get should return value', async () => { const key = Math.random().toString(); - pettyCache.set(key, 'hello world', () => { - pettyCache.get(key, (err, value) => { - assert.equal(value, 'hello world'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.equal(value, 'hello world'); - done(); - }); - }, 5001); - }); - }); + await pettyCache.set(key, 'hello world'); + + assert.equal(await pettyCache.get(key), 'hello world'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + assert.equal(await pettyCache.get(key), 'hello world'); }); - t.test('PettyCache.get should return null for missing keys', (t, done) => { + t.test('PettyCache.get should return null for missing keys', async () => { const key = Math.random().toString(); - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, null); - - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, null); - done(); - }); - }); + assert.strictEqual(await pettyCache.get(key), null); + assert.strictEqual(await pettyCache.get(key), null); }); t.test('PettyCache.get should return value (promises)', async () => { @@ -1381,70 +1046,7 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.mutex', { concurrency: true }, async (t) => { - t.test('PettyCache.mutex.lock (callbacks)', { concurrency: true }, async (t) => { - t.test('PettyCache.mutex.lock should lock for 1 second by default', (t, done) => { - const key = Math.random().toString(); - - pettyCache.mutex.lock(key, err => { - assert.ifError(err); - - pettyCache.mutex.lock(key, err => { - assert(err); - - setTimeout(() => { - pettyCache.mutex.lock(key, err => { - assert.ifError(err); - done(); - }); - }, 1001); - }); - }); - }); - - t.test('PettyCache.mutex.lock should lock for 2 seconds when ttl parameter is specified', (t, done) => { - const key = Math.random().toString(); - - pettyCache.mutex.lock(key, { ttl: 2000 }, err => { - assert.ifError(err); - - pettyCache.mutex.lock(key, err => { - assert(err); - - setTimeout(() => { - pettyCache.mutex.lock(key, err => { - assert(err); - }); - }, 1001); - - setTimeout(() => { - pettyCache.mutex.lock(key, err => { - assert.ifError(err); - done(); - }); - }, 2001); - }); - }); - }); - - t.test('PettyCache.mutex.lock should acquire a lock after retries', (t, done) => { - const key = Math.random().toString(); - - pettyCache.mutex.lock(key, { ttl: 2000 } , err => { - assert.ifError(err); - - pettyCache.mutex.lock(key, err => { - assert(err); - - pettyCache.mutex.lock(key, { retry: { interval: 500, times: 10 } }, err => { - assert.ifError(err); - done(); - }); - }); - }); - }); - }); - - t.test('PettyCache.mutex.lock (promises)', { concurrency: true }, async (t) => { + t.test('PettyCache.mutex.lock', { concurrency: true }, async (t) => { t.test('PettyCache.mutex.lock should lock for 1 second by default', async () => { const key = Math.random().toString(); @@ -1508,39 +1110,7 @@ test('petty-cache', { concurrency: true }, async (t) => { }); }); - t.test('PettyCache.mutex.unlock (callbacks)', { concurrency: true }, async (t) => { - t.test('PettyCache.mutex.unlock should unlock', (t, done) => { - const key = Math.random().toString(); - - pettyCache.mutex.lock(key, { ttl: 10000 }, err => { - assert.ifError(err); - - pettyCache.mutex.lock(key, err => { - assert(err); - - pettyCache.mutex.unlock(key, () => { - pettyCache.mutex.lock(key, err => { - assert.ifError(err); - done(); - }); - }); - }); - }); - }); - - t.test('PettyCache.mutex.unlock should work without a callback', (t, done) => { - const key = Math.random().toString(); - - pettyCache.mutex.lock(key, { ttl: 10000 }, err => { - assert.ifError(err); - - pettyCache.mutex.unlock(key); - done(); - }); - }); - }); - - t.test('PettyCache.mutex.unlock (promises)', { concurrency: true }, async (t) => { + t.test('PettyCache.mutex.unlock', { concurrency: true }, async (t) => { t.test('PettyCache.mutex.unlock should unlock', async () => { const key = Math.random().toString(); @@ -1561,45 +1131,6 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('PettyCache.patch', { concurrency: true }, async (t) => { - t.test('PettyCache.patch should fail if the key does not exist', (t, done) => { - pettyCache.patch(Math.random().toString(), { b: 3 }, (err) => { - assert(err, 'No error provided'); - done(); - }); - }); - - t.test('PettyCache.patch should update the values of given object keys', (t, done) => { - const key = Math.random().toString(); - - pettyCache.set(key, { a: 1, b: 2, c: 3 }, () => { - pettyCache.patch(key, { b: 4, c: 5 }, (err) => { - assert(!err, 'Error: ' + err); - - pettyCache.get(key, (err, data) => { - assert(!err, 'Error: ' + err); - assert.deepEqual(data, { a: 1, b: 4, c: 5 }); - done(); - }); - }); - }); - }); - - t.test('PettyCache.patch should update the values of given object keys with options', (t, done) => { - const key = Math.random().toString(); - - pettyCache.set(key, { a: 1, b: 2, c: 3 }, () => { - pettyCache.patch(key, { b: 5, c: 6 }, { ttl: 10000 }, (err) => { - assert(!err, 'Error: ' + err); - - pettyCache.get(key, (err, data) => { - assert(!err, 'Error: ' + err); - assert.deepEqual(data, { a: 1, b: 5, c: 6 }); - done(); - }); - }); - }); - }); - t.test('PettyCache.patch should update the values of given object keys (promises)', async () => { const key = Math.random().toString(); @@ -1625,534 +1156,280 @@ test('petty-cache', { concurrency: true }, async (t) => { t.test('PettyCache.semaphore', { concurrency: true }, async (t) => { t.test('PettyCache.semaphore.acquireLock', { concurrency: true }, async (t) => { - t.test('should aquire a lock', (t, done) => { + t.test('should aquire a lock', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); - done(); - }); - }); - }); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 1); }); - t.test('should not aquire a lock', (t, done) => { + t.test('should not aquire a lock', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - done(); - }); - }); - }); + await assert.rejects( + pettyCache.semaphore.acquireLock(key), + { message: `Semaphore ${key} doesn't have any available slots.` } + ); }); - t.test('should aquire a lock after ttl', (t, done) => { + t.test('should aquire a lock after ttl', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - - setTimeout(() => { - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - done(); - }); - }, 1001); - }); - }); - }); + await pettyCache.semaphore.retrieveOrCreate(key); + + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); + + await assert.rejects(pettyCache.semaphore.acquireLock(key)); + + await timers.setTimeout(1001); + + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); }); - t.test('should aquire a lock with specified options', (t, done) => { + t.test('should aquire a lock with specified options', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }); + + await pettyCache.semaphore.acquireLock(key); + + await timers.setTimeout(1000); - // callback is optional - pettyCache.semaphore.acquireLock(key); + const index = await pettyCache.semaphore.acquireLock(key, { retry: { interval: 500, times: 10 }, ttl: 500 }); - setTimeout(() => { - pettyCache.semaphore.acquireLock(key, { retry: { interval: 500, times: 10 }, ttl: 500 }, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); - done(); - }); - }, 1000); - }); + assert.equal(index, 1); }); - t.test('should fail if the semaphore does not exist', (t, done) => { + t.test('should fail if the semaphore does not exist', async () => { const key = Math.random().toString(); - pettyCache.semaphore.acquireLock(key, {}, (err) => { - assert(err); - assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); - done(); - }); + await assert.rejects( + pettyCache.semaphore.acquireLock(key, {}), + { message: `Semaphore ${key} doesn't exist.` } + ); }); }); t.test('PettyCache.semaphore.consumeLock', { concurrency: true }, async (t) => { - t.test('should consume a lock', (t, done) => { + t.test('should consume a lock', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 1); - pettyCache.semaphore.consumeLock(key, 0, (err) => { - assert.ifError(err); + await assert.rejects(pettyCache.semaphore.acquireLock(key)); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - done(); - }); - }); - }); - }); - }); - }); - }); - - t.test('should ensure at least one lock is not consumed', (t, done) => { - const key = Math.random().toString(); + await pettyCache.semaphore.consumeLock(key, 0); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); - - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - - pettyCache.semaphore.consumeLock(key, 0, (err) => { - assert.ifError(err); - - pettyCache.semaphore.consumeLock(key, 1, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err) => { - assert.ifError(err); - assert.equal(index, 1); - done(); - }); - }); - }); - }); - }); - }); - }); + await assert.rejects(pettyCache.semaphore.acquireLock(key)); }); - t.test('should fail if the semaphore does not exist', (t, done) => { + t.test('should ensure at least one lock is not consumed', async () => { const key = Math.random().toString(); - pettyCache.semaphore.consumeLock(key, 0, (err) => { - assert(err); - assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); - done(); - }); - }); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - t.test('should fail if index is larger than semaphore', (t, done) => { - const key = Math.random().toString(); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 1); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); + await assert.rejects(pettyCache.semaphore.acquireLock(key)); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + await pettyCache.semaphore.consumeLock(key, 0); + await pettyCache.semaphore.consumeLock(key, 1); - pettyCache.semaphore.consumeLock(key, 10, (err) => { - assert(err); - assert.strictEqual(err.message, `Index 10 for semaphore ${key} is invalid.`); - done(); - }); - }); - }); + assert.equal(await pettyCache.semaphore.acquireLock(key), 1); }); - t.test('callback is optional', (t, done) => { + t.test('should fail if the semaphore does not exist', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + await assert.rejects( + pettyCache.semaphore.consumeLock(key, 0), + { message: `Semaphore ${key} doesn't exist.` } + ); + }); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); + t.test('should fail if index is larger than semaphore', async () => { + const key = Math.random().toString(); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.consumeLock(key, 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - done(); - }); - }); - }); - }); - }); + await assert.rejects( + pettyCache.semaphore.consumeLock(key, 10), + { message: `Index 10 for semaphore ${key} is invalid.` } + ); }); }); t.test('PettyCache.semaphore.expand', { concurrency: true }, async (t) => { - t.test('should increase the size of a semaphore pool', (t, done) => { + t.test('should increase the size of a semaphore pool', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 2); - - pettyCache.semaphore.expand(key, 3, (err) => { - assert.ifError(err); + const pool = await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 3); - done(); - }); - }); - }); - }); + assert.strictEqual(pool.length, 2); - t.test('should refuse to shrink a pool', (t, done) => { - const key = Math.random().toString(); + await pettyCache.semaphore.expand(key, 3); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 2); + const expanded = await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.expand(key, 1, (err) => { - assert(err); - assert.strictEqual(err.message, 'Cannot shrink pool, size is 2 and you requested a size of 1.'); - done(); - }); - }); + assert.strictEqual(expanded.length, 3); }); - t.test('should succeed if pool size is already equal to the specified size', (t, done) => { + t.test('should refuse to shrink a pool', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 2); + const pool = await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.expand(key, 2, (err) => { - assert.ifError(err); + assert.strictEqual(pool.length, 2); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 2); - done(); - }); - }); - }); + await assert.rejects( + pettyCache.semaphore.expand(key, 1), + { message: 'Cannot shrink pool, size is 2 and you requested a size of 1.' } + ); }); - t.test('callback is optional', (t, done) => { + t.test('should succeed if pool size is already equal to the specified size', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 2); + const pool = await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); + + assert.strictEqual(pool.length, 2); + + await pettyCache.semaphore.expand(key, 2); - pettyCache.semaphore.expand(key, 3); + const unchanged = await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err, pool) => { - assert.ifError(err); - assert.strictEqual(pool.length, 3); - done(); - }); - }); + assert.strictEqual(unchanged.length, 2); }); - t.test('should fail if the semaphore does not exist', (t, done) => { + t.test('should fail if the semaphore does not exist', async () => { const key = Math.random().toString(); - pettyCache.semaphore.expand(key, 10, (err) => { - assert(err); - assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); - done(); - }); + await assert.rejects( + pettyCache.semaphore.expand(key, 10), + { message: `Semaphore ${key} doesn't exist.` } + ); }); }); t.test('PettyCache.semaphore.releaseLock', { concurrency: true }, async (t) => { - t.test('should release a lock', (t, done) => { + t.test('should release a lock', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); + await pettyCache.semaphore.retrieveOrCreate(key); - pettyCache.semaphore.releaseLock(key, 0, (err) => { - assert.ifError(err); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - done(); - }); - }); - }); - }); - }); - }); - - t.test('should fail to release a lock outside of the semaphore size', (t, done) => { - const key = Math.random().toString(); - - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await assert.rejects(pettyCache.semaphore.acquireLock(key)); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + await pettyCache.semaphore.releaseLock(key, 0); - pettyCache.semaphore.releaseLock(key, 10, (err) => { - assert(err); - assert.strictEqual(err.message, `Index 10 for semaphore ${key} is invalid.`); - done(); - }); - }); - }); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); }); - t.test('callback is optional', (t, done) => { + t.test('should fail to release a lock outside of the semaphore size', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - - pettyCache.semaphore.releaseLock(key, 0); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - done(); - }); - }); - }); - }); + await assert.rejects( + pettyCache.semaphore.releaseLock(key, 10), + { message: `Index 10 for semaphore ${key} is invalid.` } + ); }); - t.test('should fail if the semaphore does not exist', (t, done) => { + t.test('should fail if the semaphore does not exist', async () => { const key = Math.random().toString(); - pettyCache.semaphore.releaseLock(key, 10, (err) => { - assert(err); - assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); - done(); - }); + await assert.rejects( + pettyCache.semaphore.releaseLock(key, 10), + { message: `Semaphore ${key} doesn't exist.` } + ); }); }); t.test('PettyCache.semaphore.reset', { concurrency: true }, async (t) => { - t.test('should reset all locks', (t, done) => { + t.test('should reset all locks', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); - - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); - - pettyCache.semaphore.reset(key, (err) => { - assert.ifError(err); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - done(); - }); - }); - }); - }); - }); - }); - }); - - t.test('callback is optional', (t, done) => { - const key = Math.random().toString(); - - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); + assert.equal(await pettyCache.semaphore.acquireLock(key), 1); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 1); + await assert.rejects(pettyCache.semaphore.acquireLock(key)); - pettyCache.semaphore.acquireLock(key, (err) => { - assert(err); + await pettyCache.semaphore.reset(key); - pettyCache.semaphore.reset(key); - - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); - assert.equal(index, 0); - done(); - }); - }); - }); - }); - }); + assert.equal(await pettyCache.semaphore.acquireLock(key), 0); }); - t.test('should fail if the semaphore does not exist', (t, done) => { + t.test('should fail if the semaphore does not exist', async () => { const key = Math.random().toString(); - pettyCache.semaphore.reset(key, (err) => { - assert(err); - assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); - done(); - }); + await assert.rejects( + pettyCache.semaphore.reset(key), + { message: `Semaphore ${key} doesn't exist.` } + ); }); }); t.test('PettyCache.semaphore.retrieveOrCreate', { concurrency: true }, async (t) => { - t.test('should create a new semaphore', (t, done) => { + t.test('should create a new semaphore', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 100); - assert(semaphore.every(s => s.status === 'available')); - - pettyCache.semaphore.retrieveOrCreate(key, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 100); - assert(semaphore.every(s => s.status === 'available')); - done(); - }); - }); - }); + const semaphore = await pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }); - t.test('should have a min size of 1', (t, done) => { - const key = Math.random().toString(); + assert(semaphore); + assert.equal(semaphore.length, 100); + assert(semaphore.every(s => s.status === 'available')); + + const retrieved = await pettyCache.semaphore.retrieveOrCreate(key); - pettyCache.semaphore.retrieveOrCreate(key, { size: 0 }, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 1); - assert(semaphore.every(s => s.status === 'available')); - - pettyCache.semaphore.retrieveOrCreate(key, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 1); - assert(semaphore.every(s => s.status === 'available')); - done(); - }); - }); + assert(retrieved); + assert.equal(retrieved.length, 100); + assert(retrieved.every(s => s.status === 'available')); }); - t.test('should allow options.size to provide a function', (t, done) => { + t.test('should have a min size of 1', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: (callback) => callback(null, 1 + 1) }, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 2); - assert(semaphore.every(s => s.status === 'available')); - - pettyCache.semaphore.retrieveOrCreate(key, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 2); - assert(semaphore.every(s => s.status === 'available')); - done(); - }); - }); + const semaphore = await pettyCache.semaphore.retrieveOrCreate(key, { size: 0 }); + + assert(semaphore); + assert.equal(semaphore.length, 1); + assert(semaphore.every(s => s.status === 'available')); + + const retrieved = await pettyCache.semaphore.retrieveOrCreate(key); + + assert(retrieved); + assert.equal(retrieved.length, 1); + assert(retrieved.every(s => s.status === 'available')); }); - t.test('callback is optional', (t, done) => { + t.test('should retrieve an existing semaphore regardless of the specified size', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key); + await pettyCache.semaphore.retrieveOrCreate(key); + + const semaphore = await pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }); - pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }, (err, semaphore) => { - assert.ifError(err); - assert(semaphore); - assert.equal(semaphore.length, 1); - assert(semaphore.every(s => s.status === 'available')); - done(); - }); + assert(semaphore); + assert.equal(semaphore.length, 1); + assert(semaphore.every(s => s.status === 'available')); }); }); @@ -2193,301 +1470,205 @@ test('petty-cache', { concurrency: true }, async (t) => { ); }); - t.test('PettyCache.semaphore.acquireLock should retry until a slot becomes available (promises)', async () => { + t.test('PettyCache.semaphore.acquireLock should retry until a slot becomes available (promises)', async () => { + const key = Math.random().toString(); + + await pettyCache.semaphore.retrieveOrCreate(key, { size: 1 }); + + // Acquire the pool's only slot with a short TTL + const index = await pettyCache.semaphore.acquireLock(key, { ttl: 500 }); + + assert.strictEqual(index, 0); + + // Retries until the first lock's TTL expires and its slot can be reclaimed + const retriedIndex = await pettyCache.semaphore.acquireLock(key, { retry: { interval: 200, times: 10 } }); + + assert.strictEqual(retriedIndex, 0); + }); + + t.test('PettyCache.semaphore.expand should reject when shrinking (promises)', async () => { + const key = Math.random().toString(); + + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); + + await assert.rejects( + pettyCache.semaphore.expand(key, 1), + { message: /Cannot shrink pool/ } + ); + }); + }); + + t.test('PettyCache.set', { concurrency: true }, async (t) => { + t.test('PettyCache.set should set a value', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, 'hello world'); + + assert.equal(await pettyCache.get(key), 'hello world'); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + assert.equal(await pettyCache.get(key), 'hello world'); + }); + + t.test('PettyCache.set should set a value with the specified TTL option', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, 'hello world', { ttl: 6000 }); + + assert.equal(await pettyCache.get(key), 'hello world'); + + // Wait for Redis cache to expire + await timers.setTimeout(6001); + + assert.equal(await pettyCache.get(key), null); + }); + + t.test('PettyCache.set should set a value with the specified TTL option using max and min', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, 'hello world', { ttl: { max: 7000, min: 6000 } }); + + assert.strictEqual(await pettyCache.get(key), 'hello world'); + + // Get again before cache expires + await timers.setTimeout(1000); + + assert.strictEqual(await pettyCache.get(key), 'hello world'); + + // Wait for Redis cache to expire + await timers.setTimeout(6001); + + assert.strictEqual(await pettyCache.get(key), null); + }); + + t.test('PettyCache.set should set a value with the specified TTL option using min only', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, 'hello world', { ttl: { min: 6000 } }); + + assert.strictEqual(await pettyCache.get(key), 'hello world'); + }); + + t.test('PettyCache.set should set a value with the specified TTL option using max only', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, 'hello world', { ttl: { max: 10000 } }); + + assert.strictEqual(await pettyCache.get(key), 'hello world'); + }); + + t.test('PettyCache.set(key, \'\')', async () => { + const key = Math.random().toString(); + + await pettyCache.set(key, '', { ttl: 7000 }); + + assert.strictEqual(await pettyCache.get(key), ''); + + // Wait for memory cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), ''); + + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); + }); + + t.test('PettyCache.set(key, 0)', async () => { const key = Math.random().toString(); - await pettyCache.semaphore.retrieveOrCreate(key, { size: 1 }); + await pettyCache.set(key, 0, { ttl: 7000 }); - // Acquire the pool's only slot with a short TTL - const index = await pettyCache.semaphore.acquireLock(key, { ttl: 500 }); + assert.strictEqual(await pettyCache.get(key), 0); - assert.strictEqual(index, 0); + // Wait for memory cache to expire + await timers.setTimeout(5001); - // Retries until the first lock's TTL expires and its slot can be reclaimed - const retriedIndex = await pettyCache.semaphore.acquireLock(key, { retry: { interval: 200, times: 10 } }); + assert.strictEqual(await pettyCache.get(key), 0); - assert.strictEqual(retriedIndex, 0); + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); }); - t.test('PettyCache.semaphore.expand should reject when shrinking (promises)', async () => { + t.test('PettyCache.set(key, false)', async () => { const key = Math.random().toString(); - await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); + await pettyCache.set(key, false, { ttl: 7000 }); - await assert.rejects( - pettyCache.semaphore.expand(key, 1), - { message: /Cannot shrink pool/ } - ); - }); - }); + assert.strictEqual(await pettyCache.get(key), false); - t.test('PettyCache.set', { concurrency: true }, async (t) => { - t.test('PettyCache.set should set a value', (t, done) => { - const key = Math.random().toString(); + // Wait for memory cache to expire + await timers.setTimeout(5001); - pettyCache.set(key, 'hello world', () => { - pettyCache.get(key, (err, value) => { - assert.equal(value, 'hello world'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.equal(value, 'hello world'); - done(); - }); - }, 5001); - }); - }); - }); + assert.strictEqual(await pettyCache.get(key), false); - t.test('PettyCache.set should work without a callback', (t, done) => { - pettyCache.set(Math.random().toString(), 'hello world'); - done(); + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); }); - t.test('PettyCache.set should set a value with the specified TTL option', (t, done) => { + t.test('PettyCache.set(key, NaN)', async () => { const key = Math.random().toString(); - pettyCache.set(key, 'hello world', { ttl: 6000 },() => { - pettyCache.get(key, (err, value) => { - assert.equal(value, 'hello world'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.equal(value, null); - done(); - }); - }, 6001); - }); - }); - }); + await pettyCache.set(key, NaN, { ttl: 7000 }); - t.test('PettyCache.set should set a value with the specified TTL option using max and min', (t, done) => { - const key = Math.random().toString(); + const value = await pettyCache.get(key); - pettyCache.set(key, 'hello world', { ttl: { max: 7000, min: 6000 } },() => { - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, 'hello world'); - - // Get again before cache expires - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, 'hello world'); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, null); - done(); - }); - }, 6001); - }); - }, 1000); - }); - }); - }); + assert(typeof value === 'number' && isNaN(value)); - t.test('PettyCache.set should set a value with the specified TTL option using min only', (t, done) => { - const key = Math.random().toString(); + // Wait for memory cache to expire + await timers.setTimeout(5001); - pettyCache.set(key, 'hello world', { ttl: { min: 6000 } },() => { - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, 'hello world'); - done(); - }); - }); - }); + const fromRedis = await pettyCache.get(key); - t.test('PettyCache.set should set a value with the specified TTL option using max only', (t, done) => { - const key = Math.random().toString(); + assert(typeof fromRedis === 'number' && isNaN(fromRedis)); - pettyCache.set(key, 'hello world', { ttl: { max: 10000 } },() => { - pettyCache.get(key, (err, value) => { - assert.strictEqual(value, 'hello world'); - done(); - }); - }); + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); }); - t.test('PettyCache.set(key, \'\')', (t, done) => { + t.test('PettyCache.set(key, null)', async () => { const key = Math.random().toString(); - pettyCache.set(key, '', { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, ''); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, ''); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); - }); + await pettyCache.set(key, null, { ttl: 7000 }); - t.test('PettyCache.set(key, 0)', (t, done) => { - const key = Math.random().toString(); + assert.strictEqual(await pettyCache.get(key), null); - pettyCache.set(key, 0, { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 0); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 0); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); - }); + // Wait for memory cache to expire + await timers.setTimeout(5001); - t.test('PettyCache.set(key, false)', (t, done) => { - const key = Math.random().toString(); + assert.strictEqual(await pettyCache.get(key), null); - pettyCache.set(key, false, { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, false); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, false); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); }); - t.test('PettyCache.set(key, NaN)', (t, done) => { + t.test('PettyCache.set(key, undefined)', async () => { const key = Math.random().toString(); - pettyCache.set(key, NaN, { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert(typeof value === 'number' && isNaN(value)); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert(typeof value === 'number' && isNaN(value)); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); - }); + await pettyCache.set(key, undefined, { ttl: 7000 }); - t.test('PettyCache.set(key, null)', (t, done) => { - const key = Math.random().toString(); + assert.strictEqual(await pettyCache.get(key), undefined); - pettyCache.set(key, null, { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); - }); + // Wait for memory cache to expire + await timers.setTimeout(5001); - t.test('PettyCache.set(key, undefined)', (t, done) => { - const key = Math.random().toString(); + assert.strictEqual(await pettyCache.get(key), undefined); - pettyCache.set(key, undefined, { ttl: 7000 }, (err) => { - assert.ifError(err); - - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, undefined); - - // Wait for memory cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, undefined); - - // Wait for memory cache and Redis cache to expire - setTimeout(() => { - pettyCache.get(key, (err, value) => { - assert.ifError(err); - assert.strictEqual(value, null); - done(); - }); - }, 5001); - }); - }, 5001); - }); - }); + // Wait for memory cache and Redis cache to expire + await timers.setTimeout(5001); + + assert.strictEqual(await pettyCache.get(key), null); }); t.test('PettyCache.set should set a value (promises)', async () => { @@ -2520,7 +1701,7 @@ test('petty-cache', { concurrency: true }, async (t) => { }); t.test('redisClient', { concurrency: true }, async (t) => { - t.test('redisClient.mget(falsy keys)', (t, done) => { + t.test('redisClient.mGet(falsy keys)', async () => { const key1 = Math.random().toString(); const key2 = Math.random().toString(); const key3 = Math.random().toString(); @@ -2536,172 +1717,126 @@ test('petty-cache', { concurrency: true }, async (t) => { values[key5] = null; values[key6] = undefined; - Promise.all(Object.keys(values).map(key => new Promise((resolve, reject) => { - redisClient.psetex(key, 100, PettyCache.stringify(values[key]), err => err ? reject(err) : resolve()); - }))).then(() => { - const keys = Object.keys(values); - - // Add an additional key to check handling of missing keys - keys.push(Math.random().toString()); - - redisClient.mget(keys, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.length, 7); - assert.strictEqual(data[0], '""'); - assert.strictEqual(PettyCache.parse(data[0]), ''); - assert.strictEqual(data[1], '0'); - assert.strictEqual(PettyCache.parse(data[1]), 0); - assert.strictEqual(data[2], 'false'); - assert.strictEqual(PettyCache.parse(data[2]), false); - assert.strictEqual(data[3], '"__NaN"'); - assert.strictEqual(typeof PettyCache.parse(data[3]), 'number'); - assert(isNaN(PettyCache.parse(data[3]))); - assert.strictEqual(data[4], '"__null"'); - assert.strictEqual(PettyCache.parse(data[4]), null); - assert.strictEqual(data[5], '"__undefined"'); - assert.strictEqual(PettyCache.parse(data[5]), undefined); - assert.strictEqual(data[6], null); - done(); - }); - }); + await Promise.all(Object.keys(values).map(key => redisClient.pSetEx(key, 100, PettyCache.stringify(values[key])))); + + const keys = Object.keys(values); + + // Add an additional key to check handling of missing keys + keys.push(Math.random().toString()); + + const data = await redisClient.mGet(keys); + + assert.strictEqual(data.length, 7); + assert.strictEqual(data[0], '""'); + assert.strictEqual(PettyCache.parse(data[0]), ''); + assert.strictEqual(data[1], '0'); + assert.strictEqual(PettyCache.parse(data[1]), 0); + assert.strictEqual(data[2], 'false'); + assert.strictEqual(PettyCache.parse(data[2]), false); + assert.strictEqual(data[3], '"__NaN"'); + assert.strictEqual(typeof PettyCache.parse(data[3]), 'number'); + assert(isNaN(PettyCache.parse(data[3]))); + assert.strictEqual(data[4], '"__null"'); + assert.strictEqual(PettyCache.parse(data[4]), null); + assert.strictEqual(data[5], '"__undefined"'); + assert.strictEqual(PettyCache.parse(data[5]), undefined); + assert.strictEqual(data[6], null); }); - t.test('redisClient.psetex(key, \'\')', (t, done) => { + t.test('redisClient.pSetEx(key, \'\')', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(''), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, '""'); - assert.strictEqual(PettyCache.parse(data), ''); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify('')); + + const data = await redisClient.get(key); + + assert.strictEqual(data, '""'); + assert.strictEqual(PettyCache.parse(data), ''); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); - t.test('redisClient.psetex(key, 0)', (t, done) => { + t.test('redisClient.pSetEx(key, 0)', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(0), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, '0'); - assert.strictEqual(PettyCache.parse(data), 0); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify(0)); + + const data = await redisClient.get(key); + + assert.strictEqual(data, '0'); + assert.strictEqual(PettyCache.parse(data), 0); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); - t.test('redisClient.psetex(key, false)', (t, done) => { + t.test('redisClient.pSetEx(key, false)', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(false), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, 'false'); - assert.strictEqual(PettyCache.parse(data), false); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify(false)); + + const data = await redisClient.get(key); + + assert.strictEqual(data, 'false'); + assert.strictEqual(PettyCache.parse(data), false); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); - t.test('redisClient.psetex(key, NaN)', (t, done) => { + t.test('redisClient.pSetEx(key, NaN)', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(NaN), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, '"__NaN"'); - assert(isNaN(PettyCache.parse(data))); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify(NaN)); + + const data = await redisClient.get(key); + + assert.strictEqual(data, '"__NaN"'); + assert(isNaN(PettyCache.parse(data))); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); - t.test('redisClient.psetex(key, null)', (t, done) => { + t.test('redisClient.pSetEx(key, null)', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(null), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, '"__null"'); - assert.strictEqual(PettyCache.parse(data), null); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify(null)); + + const data = await redisClient.get(key); + + assert.strictEqual(data, '"__null"'); + assert.strictEqual(PettyCache.parse(data), null); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); - t.test('redisClient.psetex(key, undefined)', (t, done) => { + t.test('redisClient.pSetEx(key, undefined)', async () => { const key = Math.random().toString(); - redisClient.psetex(key, 100, PettyCache.stringify(undefined), (err) => { - assert.ifError(err); - - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, '"__undefined"'); - assert.strictEqual(PettyCache.parse(data), undefined); - - // Wait for Redis cache to expire - setTimeout(() => { - redisClient.get(key, (err, data) => { - assert.ifError(err); - assert.strictEqual(data, null); - done(); - }); - }, 101); - }); - }); + await redisClient.pSetEx(key, 100, PettyCache.stringify(undefined)); + + const data = await redisClient.get(key); + + assert.strictEqual(data, '"__undefined"'); + assert.strictEqual(PettyCache.parse(data), undefined); + + // Wait for Redis cache to expire + await timers.setTimeout(101); + + assert.strictEqual(await redisClient.get(key), null); }); }); @@ -2713,19 +1848,9 @@ test('petty-cache', { concurrency: true }, async (t) => { const redisKey = Math.random().toString(); const redisStart = Date.now(); - await new Promise((resolve, reject) => { - redisClient.psetex(redisKey, 30000, JSON.stringify(emojis), err => err ? reject(err) : resolve()); - }); - - await Promise.all(Array.from({ length: 500 }, () => new Promise((resolve, reject) => { - redisClient.get(redisKey, (err, data) => { - if (err) { - return reject(err); - } + await redisClient.pSetEx(redisKey, 30000, JSON.stringify(emojis)); - resolve(JSON.parse(data)); - }); - }))); + await Promise.all(Array.from({ length: 500 }, () => redisClient.get(redisKey).then(data => JSON.parse(data)))); const redisEnd = Date.now(); const pettyCacheStart = Date.now(); @@ -2740,879 +1865,716 @@ test('petty-cache', { concurrency: true }, async (t) => { }); }); -test('PettyCache.fetch should return error if Redis GET fails', (t, done) => { +test('PettyCache.fetch should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.fetch(Math.random().toString(), (callback) => { - callback(null, 'value'); - }, (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.fetch(Math.random().toString(), async () => 'value'), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.get should return error if Redis GET fails', (t, done) => { +test('PettyCache.get should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.get(Math.random().toString(), (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.get(Math.random().toString()), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.bulkFetch should return error if Redis MGET fails', (t, done) => { +test('PettyCache.bulkFetch should return error if Redis MGET fails', async () => { const stubClient = redis.createClient(); - const originalMget = stubClient.mget.bind(stubClient); + const originalMGet = stubClient.mGet.bind(stubClient); - stubClient.mget = (keys, callback) => callback(new Error('Redis MGET error')); + stubClient.mGet = () => Promise.reject(new Error('Redis MGET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.bulkFetch([Math.random().toString()], (keys, callback) => { - callback(null, {}); - }, (err) => { - stubClient.mget = originalMget; - - assert(err); - assert.strictEqual(err.message, 'Redis MGET error'); + await assert.rejects( + pettyCache.bulkFetch([Math.random().toString()], async () => ({})), + { message: 'Redis MGET error' } + ); - done(); - }); + stubClient.mGet = originalMGet; }); -test('PettyCache.bulkGet should return error if Redis MGET fails', (t, done) => { +test('PettyCache.bulkGet should return error if Redis MGET fails', async () => { const stubClient = redis.createClient(); - const originalMget = stubClient.mget.bind(stubClient); + const originalMGet = stubClient.mGet.bind(stubClient); - stubClient.mget = (keys, callback) => callback(new Error('Redis MGET error')); + stubClient.mGet = () => Promise.reject(new Error('Redis MGET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.bulkGet([Math.random().toString()], (err) => { - stubClient.mget = originalMget; - - assert(err); - assert.strictEqual(err.message, 'Redis MGET error'); + await assert.rejects( + pettyCache.bulkGet([Math.random().toString()]), + { message: 'Redis MGET error' } + ); - done(); - }); + stubClient.mGet = originalMGet; }); -test('PettyCache.bulkSet should return error if Redis batch exec fails', (t, done) => { +test('PettyCache.bulkSet should return error if Redis PSETEX fails', async () => { const stubClient = redis.createClient(); - const originalBatch = stubClient.batch.bind(stubClient); + const originalPSetEx = stubClient.pSetEx.bind(stubClient); - stubClient.batch = () => { - const batch = originalBatch(); - batch.exec = (callback) => callback(new Error('Redis EXEC error')); - return batch; - }; + stubClient.pSetEx = () => Promise.reject(new Error('Redis PSETEX error')); const pettyCache = new PettyCache(stubClient); const values = {}; values[Math.random().toString()] = 'value'; - pettyCache.bulkSet(values, (err) => { - stubClient.batch = originalBatch; + await assert.rejects( + pettyCache.bulkSet(values), + { message: 'Redis PSETEX error' } + ); - assert(err); - assert.strictEqual(err.message, 'Redis EXEC error'); - - done(); - }); + stubClient.pSetEx = originalPSetEx; }); -test('PettyCache.bulkFetch should return error if bulkSet fails', (t, done) => { +test('PettyCache.bulkFetch should return error if bulkSet fails', async () => { const stubClient = redis.createClient(); - const originalBatch = stubClient.batch.bind(stubClient); + const originalPSetEx = stubClient.pSetEx.bind(stubClient); - stubClient.batch = () => { - const batch = originalBatch(); - batch.exec = (callback) => callback(new Error('Redis EXEC error')); - return batch; - }; + stubClient.pSetEx = () => Promise.reject(new Error('Redis PSETEX error')); const pettyCache = new PettyCache(stubClient); const key = Math.random().toString(); - pettyCache.bulkFetch([key], (keys, callback) => { - const data = {}; - data[keys[0]] = 'value'; - callback(null, data); - }, (err) => { - stubClient.batch = originalBatch; - - assert(err); - assert.strictEqual(err.message, 'Redis EXEC error'); + await assert.rejects( + pettyCache.bulkFetch([key], async (keys) => { + const data = {}; + data[keys[0]] = 'value'; + return data; + }), + { message: 'Redis PSETEX error' } + ); - done(); - }); + stubClient.pSetEx = originalPSetEx; }); -test('PettyCache.mutex.lock should return error if Redis SET fails', (t, done) => { +test('PettyCache.mutex.lock should return error if Redis SET fails', async () => { const stubClient = redis.createClient(); const originalSet = stubClient.set.bind(stubClient); - stubClient.set = (...args) => args[args.length - 1](new Error('Redis SET error')); + stubClient.set = () => Promise.reject(new Error('Redis SET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.mutex.lock(Math.random().toString(), (err) => { - stubClient.set = originalSet; - - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + pettyCache.mutex.lock(Math.random().toString()), + { message: 'Redis SET error' } + ); - done(); - }); + stubClient.set = originalSet; }); -test('PettyCache.del should return error if Redis DEL fails', (t, done) => { +test('PettyCache.del should return error if Redis DEL fails', async () => { const stubClient = redis.createClient(); const originalDel = stubClient.del.bind(stubClient); - stubClient.del = (key, callback) => callback(new Error('Redis DEL error')); + stubClient.del = () => Promise.reject(new Error('Redis DEL error')); const pettyCache = new PettyCache(stubClient); - pettyCache.del(Math.random().toString(), (err) => { - stubClient.del = originalDel; - - assert(err); - assert.strictEqual(err.message, 'Redis DEL error'); + await assert.rejects( + pettyCache.del(Math.random().toString()), + { message: 'Redis DEL error' } + ); - done(); - }); + stubClient.del = originalDel; }); -test('PettyCache.mutex.unlock should return error if Redis DEL fails', (t, done) => { +test('PettyCache.mutex.unlock should return error if Redis DEL fails', async () => { const stubClient = redis.createClient(); const originalDel = stubClient.del.bind(stubClient); - stubClient.del = (key, callback) => callback(new Error('Redis DEL error')); + stubClient.del = () => Promise.reject(new Error('Redis DEL error')); const pettyCache = new PettyCache(stubClient); - pettyCache.mutex.unlock(Math.random().toString(), (err) => { - stubClient.del = originalDel; - - assert(err); - assert.strictEqual(err.message, 'Redis DEL error'); + await assert.rejects( + pettyCache.mutex.unlock(Math.random().toString()), + { message: 'Redis DEL error' } + ); - done(); - }); + stubClient.del = originalDel; }); -test('PettyCache.patch should return error if Redis GET fails', (t, done) => { +test('PettyCache.patch should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.patch(Math.random().toString(), { a: 1 }, (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.patch(Math.random().toString(), { a: 1 }), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.mutex.lock should return error if Redis SET returns unexpected response', (t, done) => { +test('PettyCache.mutex.lock should return error if Redis SET returns unexpected response', async () => { const stubClient = redis.createClient(); const originalSet = stubClient.set.bind(stubClient); - stubClient.set = (...args) => args[args.length - 1](null, 'UNEXPECTED'); + stubClient.set = () => Promise.resolve('UNEXPECTED'); const pettyCache = new PettyCache(stubClient); - pettyCache.mutex.lock(Math.random().toString(), (err) => { - stubClient.set = originalSet; + await assert.rejects( + pettyCache.mutex.lock(Math.random().toString()), + { message: 'UNEXPECTED' } + ); - assert(err); - assert.strictEqual(err.message, 'UNEXPECTED'); - - done(); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.retrieveOrCreate should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.retrieveOrCreate should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.retrieveOrCreate(Math.random().toString(), (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.retrieveOrCreate(Math.random().toString()), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.acquireLock should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.acquireLock should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.acquireLock(Math.random().toString(), (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.acquireLock(Math.random().toString()), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.consumeLock should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.consumeLock should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.consumeLock(Math.random().toString(), 0, (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.consumeLock(Math.random().toString(), 0), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.expand should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.expand should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.expand(Math.random().toString(), 10, (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.expand(Math.random().toString(), 10), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.releaseLock should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.releaseLock should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.releaseLock(Math.random().toString(), 0, (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.releaseLock(Math.random().toString(), 0), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.reset should return error if Redis GET fails', (t, done) => { +test('PettyCache.semaphore.reset should return error if Redis GET fails', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); - stubClient.get = (key, callback) => callback(new Error('Redis GET error')); + stubClient.get = () => Promise.reject(new Error('Redis GET error')); const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.reset(Math.random().toString(), (err) => { - stubClient.get = originalGet; - - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.semaphore.reset(Math.random().toString()), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.semaphore.retrieveOrCreate should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.retrieveOrCreate should return error if Redis SET fails', async () => { const stubClient = redis.createClient(); const originalSet = stubClient.set.bind(stubClient); stubClient.set = (...args) => { - if (args.includes('NX')) { + if (args[2] && args[2].NX) { return originalSet(...args); } - args[args.length - 1](new Error('Redis SET error')); + return Promise.reject(new Error('Redis SET error')); }; const pettyCache = new PettyCache(stubClient); - pettyCache.semaphore.retrieveOrCreate(Math.random().toString(), (err) => { - stubClient.set = originalSet; + await assert.rejects( + pettyCache.semaphore.retrieveOrCreate(Math.random().toString()), + { message: 'Redis SET error' } + ); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); - - done(); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.acquireLock should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.acquireLock should return error if Redis SET fails', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key); - const stubClient = redis.createClient(); - const originalSet = stubClient.set.bind(stubClient); - - stubClient.set = (...args) => { - if (args.includes('NX')) { - return originalSet(...args); - } + const stubClient = redis.createClient(); + const originalSet = stubClient.set.bind(stubClient); - args[args.length - 1](new Error('Redis SET error')); - }; + stubClient.set = (...args) => { + if (args[2] && args[2].NX) { + return originalSet(...args); + } - const stubCache = new PettyCache(stubClient); + return Promise.reject(new Error('Redis SET error')); + }; - stubCache.semaphore.acquireLock(key, (err) => { - stubClient.set = originalSet; + const stubCache = new PettyCache(stubClient); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + stubCache.semaphore.acquireLock(key), + { message: 'Redis SET error' } + ); - done(); - }); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.consumeLock should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.consumeLock should return error if Redis SET fails', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); + const index = await pettyCache.semaphore.acquireLock(key); - const stubClient = redis.createClient(); - const originalSet = stubClient.set.bind(stubClient); - - stubClient.set = (...args) => { - if (args.includes('NX')) { - return originalSet(...args); - } + const stubClient = redis.createClient(); + const originalSet = stubClient.set.bind(stubClient); - args[args.length - 1](new Error('Redis SET error')); - }; + stubClient.set = (...args) => { + if (args[2] && args[2].NX) { + return originalSet(...args); + } - const stubCache = new PettyCache(stubClient); + return Promise.reject(new Error('Redis SET error')); + }; - stubCache.semaphore.consumeLock(key, index, (err) => { - stubClient.set = originalSet; + const stubCache = new PettyCache(stubClient); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + stubCache.semaphore.consumeLock(key, index), + { message: 'Redis SET error' } + ); - done(); - }); - }); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.expand should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.expand should return error if Redis SET fails', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }); - const stubClient = redis.createClient(); - const originalSet = stubClient.set.bind(stubClient); - - stubClient.set = (...args) => { - if (args.includes('NX')) { - return originalSet(...args); - } + const stubClient = redis.createClient(); + const originalSet = stubClient.set.bind(stubClient); - args[args.length - 1](new Error('Redis SET error')); - }; + stubClient.set = (...args) => { + if (args[2] && args[2].NX) { + return originalSet(...args); + } - const stubCache = new PettyCache(stubClient); + return Promise.reject(new Error('Redis SET error')); + }; - stubCache.semaphore.expand(key, 5, (err) => { - stubClient.set = originalSet; + const stubCache = new PettyCache(stubClient); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + stubCache.semaphore.expand(key, 5), + { message: 'Redis SET error' } + ); - done(); - }); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.releaseLock should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.releaseLock should return error if Redis SET fails', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key); - pettyCache.semaphore.acquireLock(key, (err, index) => { - assert.ifError(err); + const index = await pettyCache.semaphore.acquireLock(key); - const stubClient = redis.createClient(); - const originalSet = stubClient.set.bind(stubClient); - - stubClient.set = (...args) => { - if (args.includes('NX')) { - return originalSet(...args); - } + const stubClient = redis.createClient(); + const originalSet = stubClient.set.bind(stubClient); - args[args.length - 1](new Error('Redis SET error')); - }; + stubClient.set = (...args) => { + if (args[2] && args[2].NX) { + return originalSet(...args); + } - const stubCache = new PettyCache(stubClient); + return Promise.reject(new Error('Redis SET error')); + }; - stubCache.semaphore.releaseLock(key, index, (err) => { - stubClient.set = originalSet; + const stubCache = new PettyCache(stubClient); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + stubCache.semaphore.releaseLock(key, index), + { message: 'Redis SET error' } + ); - done(); - }); - }); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.reset should return error if Redis SET fails', (t, done) => { +test('PettyCache.semaphore.reset should return error if Redis SET fails', async () => { const key = Math.random().toString(); - pettyCache.semaphore.retrieveOrCreate(key, (err) => { - assert.ifError(err); + await pettyCache.semaphore.retrieveOrCreate(key); - const stubClient = redis.createClient(); - const originalSet = stubClient.set.bind(stubClient); - - stubClient.set = (...args) => { - if (args.includes('NX')) { - return originalSet(...args); - } + const stubClient = redis.createClient(); + const originalSet = stubClient.set.bind(stubClient); - args[args.length - 1](new Error('Redis SET error')); - }; + stubClient.set = (...args) => { + if (args[2] && args[2].NX) { + return originalSet(...args); + } - const stubCache = new PettyCache(stubClient); + return Promise.reject(new Error('Redis SET error')); + }; - stubCache.semaphore.reset(key, (err) => { - stubClient.set = originalSet; + const stubCache = new PettyCache(stubClient); - assert(err); - assert.strictEqual(err.message, 'Redis SET error'); + await assert.rejects( + stubCache.semaphore.reset(key), + { message: 'Redis SET error' } + ); - done(); - }); - }); + stubClient.set = originalSet; }); -test('PettyCache.semaphore.retrieveOrCreate should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.retrieveOrCreate should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.retrieveOrCreate(Math.random().toString(), (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.retrieveOrCreate(Math.random().toString()), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.acquireLock should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.acquireLock should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.acquireLock(Math.random().toString(), (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.acquireLock(Math.random().toString()), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.consumeLock should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.consumeLock should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.consumeLock(Math.random().toString(), 0, (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.consumeLock(Math.random().toString(), 0), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.expand should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.expand should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.expand(Math.random().toString(), 10, (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.expand(Math.random().toString(), 10), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.releaseLock should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.releaseLock should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.releaseLock(Math.random().toString(), 0, (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.releaseLock(Math.random().toString(), 0), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.reset should return error if mutex lock fails', (t, done) => { +test('PettyCache.semaphore.reset should return error if mutex lock fails', async () => { const stubClient = redis.createClient(); const stubCache = new PettyCache(stubClient); - stubCache.mutex.lock = (key, options, callback) => callback ? callback(new Error('mutex lock error')) : Promise.reject(new Error('mutex lock error')); + stubCache.mutex.lock = () => Promise.reject(new Error('mutex lock error')); - stubCache.semaphore.reset(Math.random().toString(), (err) => { - assert(err); - assert.strictEqual(err.message, 'mutex lock error'); - - done(); - }); + await assert.rejects( + stubCache.semaphore.reset(Math.random().toString()), + { message: 'mutex lock error' } + ); }); -test('PettyCache.semaphore.retrieveOrCreate should return error if size function fails', (t, done) => { - pettyCache.semaphore.retrieveOrCreate(Math.random().toString(), { size: (callback) => callback(new Error('size error')) }, (err) => { - assert(err); - assert.strictEqual(err.message, 'size error'); - - done(); - }); +test('PettyCache.semaphore.retrieveOrCreate should return error if size function fails', async () => { + await assert.rejects( + pettyCache.semaphore.retrieveOrCreate(Math.random().toString(), { size: async () => { throw new Error('size error'); } }), + { message: 'size error' } + ); }); -test('PettyCache.fetch should return error if inner Redis GET fails (double-checked lock)', (t, done) => { +test('PettyCache.fetch should return error if inner Redis GET fails (double-checked lock)', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); let getCallCount = 0; - stubClient.get = (...args) => { + stubClient.get = () => { getCallCount++; if (getCallCount === 1) { - const callback = args[args.length - 1]; - return callback(null, null); + return Promise.resolve(null); } stubClient.get = originalGet; - const callback = args[args.length - 1]; - callback(new Error('Redis GET error')); + return Promise.reject(new Error('Redis GET error')); }; const pettyCache = new PettyCache(stubClient); - pettyCache.fetch(Math.random().toString(), (callback) => { - callback(null, 'value'); - }, (err) => { - stubClient.get = originalGet; - assert(err); - assert.strictEqual(err.message, 'Redis GET error'); + await assert.rejects( + pettyCache.fetch(Math.random().toString(), async () => 'value'), + { message: 'Redis GET error' } + ); - done(); - }); + stubClient.get = originalGet; }); -test('PettyCache.fetch should return cached value from inner Redis GET (double-checked lock)', (t, done) => { +test('PettyCache.fetch should return cached value from inner Redis GET (double-checked lock)', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); let getCallCount = 0; - stubClient.get = (...args) => { + stubClient.get = () => { getCallCount++; if (getCallCount === 1) { - const callback = args[args.length - 1]; - return callback(null, null); + return Promise.resolve(null); } stubClient.get = originalGet; - const callback = args[args.length - 1]; - callback(null, JSON.stringify('cached-value')); + return Promise.resolve(JSON.stringify('cached-value')); }; const pettyCache = new PettyCache(stubClient); - pettyCache.fetch(Math.random().toString(), (callback) => { - callback(null, 'should-not-be-used'); - }, (err, value) => { - stubClient.get = originalGet; - assert.ifError(err); - assert.strictEqual(value, 'cached-value'); + const value = await pettyCache.fetch(Math.random().toString(), async () => 'should-not-be-used'); - done(); - }); + assert.strictEqual(value, 'cached-value'); + + stubClient.get = originalGet; }); -test('PettyCache.fetch should return cached value from inner memory cache (double-checked lock)', (t, done) => { +test('PettyCache.fetch should return cached value from inner memory cache (double-checked lock)', async () => { const stubClient = redis.createClient(); const originalGet = stubClient.get.bind(stubClient); const key = Math.random().toString(); let stubCache; - stubClient.get = (...args) => { + stubClient.get = () => { stubClient.get = originalGet; // Populate memory cache synchronously via set before returning - stubCache.set(key, 'cached-value', () => {}); + stubCache.set(key, 'cached-value').catch(() => {}); - const callback = args[args.length - 1]; - callback(null, null); + return Promise.resolve(null); }; stubCache = new PettyCache(stubClient); - stubCache.fetch(key, (callback) => { - callback(null, 'should-not-be-used'); - }, (err, value) => { - stubClient.get = originalGet; - assert.ifError(err); - assert.strictEqual(value, 'cached-value'); + const value = await stubCache.fetch(key, async () => 'should-not-be-used'); - done(); - }); + assert.strictEqual(value, 'cached-value'); + + stubClient.get = originalGet; }); -test('PettyCache.get should return cached value from double-checked lock', (t, done) => { +test('PettyCache.get should return cached value from double-checked lock', async () => { const key = Math.random().toString(); // Put value directly in Redis (not memory cache) - redisClient.psetex(key, 10000, JSON.stringify('test-value'), () => { - let completed = 0; + await redisClient.pSetEx(key, 10000, JSON.stringify('test-value')); - const checkDone = (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 'test-value'); - completed++; + // Two concurrent gets - second should hit memory cache inside lock + const values = await Promise.all([pettyCache.get(key), pettyCache.get(key)]); - if (completed === 2) done(); - }; - - // Two concurrent gets - second should hit memory cache inside lock - pettyCache.get(key, checkDone); - pettyCache.get(key, checkDone); - }); + assert.strictEqual(values[0], 'test-value'); + assert.strictEqual(values[1], 'test-value'); }); -test('PettyCache.set should return error if Redis PSETEX fails', (t, done) => { +test('PettyCache.set should return error if Redis PSETEX fails', async () => { const stubClient = redis.createClient(); - const originalPsetex = stubClient.psetex.bind(stubClient); + const originalPSetEx = stubClient.pSetEx.bind(stubClient); - stubClient.psetex = (...args) => { - stubClient.psetex = originalPsetex; - const callback = args[args.length - 1]; - callback(new Error('Redis PSETEX error')); + stubClient.pSetEx = () => { + stubClient.pSetEx = originalPSetEx; + return Promise.reject(new Error('Redis PSETEX error')); }; const pettyCache = new PettyCache(stubClient); - pettyCache.set(Math.random().toString(), 'value', (err) => { - stubClient.psetex = originalPsetex; - assert(err); - assert.strictEqual(err.message, 'Redis PSETEX error'); + await assert.rejects( + pettyCache.set(Math.random().toString(), 'value'), + { message: 'Redis PSETEX error' } + ); - done(); - }); + stubClient.pSetEx = originalPSetEx; }); -test('PettyCache.patch should return error if Redis PSETEX fails', (t, done) => { +test('PettyCache.patch should return error if Redis PSETEX fails', async () => { const stubClient = redis.createClient(); - const originalPsetex = stubClient.psetex.bind(stubClient); + const originalPSetEx = stubClient.pSetEx.bind(stubClient); const key = Math.random().toString(); const pettyCache = new PettyCache(stubClient); // First set a value so patch has something to patch - pettyCache.set(key, { a: 1 }, () => { - // Now stub psetex to fail on the next call (patch's inner set) - stubClient.psetex = (...args) => { - stubClient.psetex = originalPsetex; - const callback = args[args.length - 1]; - callback(new Error('Redis PSETEX error')); - }; - - pettyCache.patch(key, { b: 2 }, (err) => { - stubClient.psetex = originalPsetex; - assert(err); - assert.strictEqual(err.message, 'Redis PSETEX error'); + await pettyCache.set(key, { a: 1 }); - done(); - }); - }); -}); + // Now stub pSetEx to fail on the next call (patch's inner set) + stubClient.pSetEx = () => { + stubClient.pSetEx = originalPSetEx; + return Promise.reject(new Error('Redis PSETEX error')); + }; -test('PettyCache.fetch should lock around Redis', (t, done) => { - redisClient.info('commandstats', (err, info) => { - const lineBefore = info.split('\n').find(i => i.startsWith('cmdstat_get:')); - const tokenBefore = lineBefore.split(/:|,/).find(i => i.startsWith('calls=')); - const callsBefore = parseInt(tokenBefore.split('=')[1]); + await assert.rejects( + pettyCache.patch(key, { b: 2 }), + { message: 'Redis PSETEX error' } + ); - const key = Math.random().toString(); - let numberOfFuncCalls = 0; + stubClient.pSetEx = originalPSetEx; +}); - const func = (callback) => { - setTimeout(() => { - callback(null, ++numberOfFuncCalls); - }, 100); - }; - - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - pettyCache.fetch(key, func); - - pettyCache.fetch(key, func, (err, data) => { - assert.equal(data, 1); +test('PettyCache.fetch should lock around Redis', async () => { + const infoBefore = await redisClient.info('commandstats'); - redisClient.info('commandstats', (err, info) => { - const lineAfter = info.split('\n').find(i => i.startsWith('cmdstat_get:')); - const tokenAfter = lineAfter.split(/:|,/).find(i => i.startsWith('calls=')); - const callsAfter = parseInt(tokenAfter.split('=')[1]); + const lineBefore = infoBefore.split('\n').find(i => i.startsWith('cmdstat_get:')); + const tokenBefore = lineBefore.split(/:|,/).find(i => i.startsWith('calls=')); + const callsBefore = parseInt(tokenBefore.split('=')[1]); - assert.strictEqual(callsBefore + 2, callsAfter); + const key = Math.random().toString(); + let numberOfFuncCalls = 0; - done(); - }); - }); - }); -}); -// Runs the specified petty-cache invocation in a child process with a callback that throws on its -// first invocation, then reports how many times the callback ran and how the throw surfaced. A -// child process is required because both of those outcomes are process-wide. -function runThrowingCallback(invocation, callback) { - const script = ` - const PettyCache = require(${JSON.stringify(require.resolve('../index.js'))}); + const func = async () => { + await timers.setTimeout(100); + return ++numberOfFuncCalls; + }; - const pettyCache = new PettyCache(); + const results = await Promise.all(Array.from({ length: 10 }, () => pettyCache.fetch(key, func))); - const calls = []; - const key = Math.random().toString(); + results.forEach(data => assert.equal(data, 1)); - const report = (channel) => { - process.stdout.write(JSON.stringify({ calls: calls, channel: channel }), () => process.exit(0)); - }; + const infoAfter = await redisClient.info('commandstats'); - process.on('uncaughtException', () => report('uncaughtException')); - process.on('unhandledRejection', () => report('unhandledRejection')); + const lineAfter = infoAfter.split('\n').find(i => i.startsWith('cmdstat_get:')); + const tokenAfter = lineAfter.split(/:|,/).find(i => i.startsWith('calls=')); + const callsAfter = parseInt(tokenAfter.split('=')[1]); - const handler = () => { - calls.push('invoked'); + assert.strictEqual(callsBefore + 2, callsAfter); +}); - if (calls.length === 1) { - throw new Error('callback threw'); - } - }; +test('PettyCache should reject callback-style usage with a TypeError', async () => { + const noop = () => {}; + + await assert.rejects(pettyCache.bulkFetch(['k'], async () => ({}), noop), TypeError); + await assert.rejects(pettyCache.bulkFetch(['k'], async () => ({}), {}, noop), TypeError); + await assert.rejects(pettyCache.bulkGet(['k'], noop), TypeError); + await assert.rejects(pettyCache.bulkSet({ k: 1 }, noop), TypeError); + await assert.rejects(pettyCache.close(noop), TypeError); + await assert.rejects(pettyCache.del('k', noop), TypeError); + await assert.rejects(pettyCache.fetch('k', async () => 'v', noop), TypeError); + await assert.rejects(pettyCache.fetchAndRefresh('k', async () => 'v', noop), TypeError); + await assert.rejects(pettyCache.get('k', noop), TypeError); + await assert.rejects(pettyCache.mutex.lock('k', noop), TypeError); + await assert.rejects(pettyCache.mutex.unlock('k', noop), TypeError); + await assert.rejects(pettyCache.patch('k', { a: 1 }, noop), TypeError); + await assert.rejects(pettyCache.semaphore.acquireLock('k', noop), TypeError); + await assert.rejects(pettyCache.semaphore.consumeLock('k', 0, noop), TypeError); + await assert.rejects(pettyCache.semaphore.expand('k', 2, noop), TypeError); + await assert.rejects(pettyCache.semaphore.releaseLock('k', 0, noop), TypeError); + await assert.rejects(pettyCache.semaphore.reset('k', noop), TypeError); + await assert.rejects(pettyCache.semaphore.retrieveOrCreate('k', noop), TypeError); + await assert.rejects(pettyCache.set('k', 'v', noop), TypeError); +}); - ${invocation}; +test('PettyCache.close should stop refresh intervals and close the client', async () => { + const client = redis.createClient(); + const newPettyCache = new PettyCache(client); + const key = Math.random().toString(); - setTimeout(() => report('none'), 1000); - `; + const data = await newPettyCache.fetchAndRefresh(key, async () => 'value'); - childProcess.execFile(process.execPath, ['-e', script], (err, stdout) => { - if (err) { - return callback(err); - } + assert.strictEqual(data, 'value'); - callback(null, JSON.parse(stdout)); - }); -} - -test('PettyCache should not invoke a callback again when the callback throws', { concurrency: true }, async (t) => { - const invocations = [ - { invocation: 'pettyCache.del(key, handler)', name: 'pettyCache.del' }, - { invocation: 'pettyCache.fetchAndRefresh(key, (callback) => callback(null, { foo: \'bar\' }), handler)', name: 'pettyCache.fetchAndRefresh' }, - { invocation: 'pettyCache.get(key, handler)', name: 'pettyCache.get' }, - { invocation: 'pettyCache.set(key, { foo: \'bar\' }, handler)', name: 'pettyCache.set' } - ]; - - for (const invocation of invocations) { - t.test(invocation.name, (t, done) => { - runThrowingCallback(invocation.invocation, (err, result) => { - assert.ifError(err); - assert.deepStrictEqual(result.calls, ['invoked']); - done(); - }); - }); - } -}); + await newPettyCache.close(); -test('PettyCache should surface a throw from a callback as an uncaught exception', (t, done) => { - runThrowingCallback('pettyCache.set(key, { foo: \'bar\' }, handler)', (err, result) => { - assert.ifError(err); - assert.strictEqual(result.channel, 'uncaughtException'); - done(); - }); -}); + assert.strictEqual(client.isOpen, false); -test('PettyCache should emit deprecation warnings for callback usage', (t, done) => { - // Warnings are emitted asynchronously; by this point the suite has exercised every callback-style API - setImmediate(() => { - assert.ok(deprecationWarnings.some(message => message.includes('pettyCache.fetch:')), 'expected a deprecation warning for pettyCache.fetch'); - assert.ok(deprecationWarnings.some(message => message.includes('pettyCache.semaphore.acquireLock:')), 'expected a deprecation warning for pettyCache.semaphore.acquireLock'); - assert.ok(deprecationWarnings.some(message => message.includes('Callback-style functions')), 'expected a deprecation warning for callback-style functions'); - done(); - }); + // Closing again is a no-op + await newPettyCache.close(); });