cached-lookup
Advanced tools
Comparing version 3.0.3 to 4.0.0
205
index.js
@@ -0,10 +1,28 @@ | ||
/** | ||
* @template T | ||
*/ | ||
class CachedLookup { | ||
#lookup; | ||
#value; | ||
#promise; | ||
#updated_at = 0; | ||
#cache = {}; | ||
#promises = {}; | ||
/** | ||
* @typedef {Boolean|Number|String} SupportedArgumentTypes | ||
*/ | ||
/** | ||
* @typedef {Object} ValueRecord | ||
* @property {T} value | ||
* @property {Number} updated_at | ||
*/ | ||
/** | ||
* Creates a new CachedLookup instance with the specified lookup function. | ||
* The lookup function can be both synchronous or asynchronous. | ||
* | ||
* @param {function(...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)):T} lookup | ||
*/ | ||
constructor(lookup) { | ||
// Ensure lookup is a function type | ||
if (typeof lookup !== 'function') throw new Error('new CachedLookup(lookup) -> lookup must be a Function'); | ||
if (typeof lookup !== 'function') throw new Error('new CachedLookup(lookup) -> lookup must be a Function.'); | ||
this.#lookup = lookup; | ||
@@ -14,102 +32,175 @@ } | ||
/** | ||
* Returns whether current cache value is valid or not based on provided age in milliseconds. | ||
* Returns an identifier string based on the provided array of arguments. | ||
* | ||
* @private | ||
* @param {Number} max_age In Milliseconds | ||
* @returns {Boolean} | ||
* @param {Array<SupportedArgumentTypes>} args | ||
* @returns {String} | ||
*/ | ||
_is_cache_valid(max_age) { | ||
return this.#updated_at + max_age > Date.now(); | ||
_arguments_to_identifier(args) { | ||
return args.join(''); | ||
} | ||
/** | ||
* Cached the provided value and renews the cache availability. | ||
* Retrieves the cached value record for the provided set of arguments. | ||
* | ||
* @private | ||
* @param {Array<SupportedArgumentTypes>} args | ||
* @param {Number} max_age | ||
* @returns {ValueRecord=} | ||
*/ | ||
_cache_value(value) { | ||
this.#value = value; | ||
this.#promise = undefined; | ||
this.#updated_at = Date.now(); | ||
_get_cache(args, max_age) { | ||
// Retrieve the identifier string for the provided arguments | ||
const identifier = this._arguments_to_identifier(args); | ||
// Attempt to lookup the value record for the specified arguments | ||
const record = this.#cache[identifier]; | ||
// Return the value record if it exists and is not older than the specified maximum age | ||
if (record && record.updated_at > Date.now() - max_age) return record; | ||
} | ||
/** | ||
* Returns cached value if the cached value is not older than the specified maximum age in milliseconds. | ||
* This method automatically retrieves a fresh value if the cached value is older than the specified maximum age. | ||
* Sets the cached value record for the provided set of arguments and value. | ||
* | ||
* @param {Number} max_age In Milliseconds | ||
* @returns {Promise} | ||
* @private | ||
* @param {Array<SupportedArgumentTypes>} args | ||
* @param {T} value | ||
*/ | ||
cached(max_age) { | ||
// Ensure max_age is a valid greater than zero number | ||
if (typeof max_age !== 'number' || max_age < 0) | ||
throw new Error('CachedLookup.cached(max_age) -> max_age must be a number that is greater than zero.'); | ||
_set_cache(args, value) { | ||
// Retrieve the identifier string for the provided arguments | ||
const identifier = this._arguments_to_identifier(args); | ||
// Return value from cache if it is still valid | ||
if (this._is_cache_valid(max_age)) return Promise.resolve(this.#value); | ||
// Initialize the record structure for the specified arguments if it does not exist | ||
if (!this.#cache[identifier]) | ||
this.#cache[identifier] = { | ||
value: null, | ||
updated_at: null, | ||
}; | ||
// Initiate a lookup for a fresh value | ||
return this.fresh(); | ||
// Fill the record values with the provided value and current timestamp | ||
this.#cache[identifier].value = value; | ||
this.#cache[identifier].updated_at = Date.now(); | ||
} | ||
/** | ||
* Fetches a fresh value from the lookup handler and returns result. | ||
* @returns {Promise} | ||
* Retrieves a fresh value from the lookup and returns result. | ||
* | ||
* @private | ||
* @param {Array<SupportedArgumentTypes>} args | ||
* @returns {Promise<T>} | ||
*/ | ||
fresh() { | ||
// Return a pending promise if one exists for an in flight lookup | ||
if (this.#promise) return this.#promise; | ||
_get_fresh_value(args) { | ||
// Retrieve the identifier string for the provided arguments | ||
const identifier = this._arguments_to_identifier(args); | ||
// Create a new promise for the lookup operation and cache it locally | ||
const scope = this; | ||
this.#promise = new Promise(async (resolve, reject) => { | ||
// Safely execute lookup handler to retrieve an output value | ||
let output; | ||
// Resolve an existing promise for the specified arguments if it exists | ||
if (this.#promises[identifier]) return this.#promises[identifier]; | ||
// Initialize a new promise which will be resolved when the value is resolved | ||
const reference = this; | ||
this.#promises[identifier] = new Promise(async (resolve, reject) => { | ||
// Attempt to resolve the value for the specified arguments from the lookup | ||
let value; | ||
try { | ||
output = await scope.#lookup(); | ||
value = await reference.#lookup(...args); | ||
} catch (error) { | ||
// Reject the error from the lookup operation | ||
return reject(error); | ||
} | ||
// Cache the output value and resolve the promise | ||
scope._cache_value(output); | ||
resolve(output); | ||
// Store the resolved value in the cache for the specified arguments | ||
reference._set_cache(args, value); | ||
// Resolve the promise with the resolved value | ||
resolve(value); | ||
// Cleanup the promise for the specified arguments | ||
delete reference.#promises[identifier]; | ||
}); | ||
return this.#promise; | ||
// Return the promise to the caller | ||
return this.#promises[identifier]; | ||
} | ||
/** | ||
* Expires the current cached value marking the instance to retrieve a fresh value on next call. | ||
* Returns a cached value that is up to max_age milliseconds old for the provided set of arguments. | ||
* Falls back to a fresh value if the cache value is older than max_age. | ||
* | ||
* @param {Number} max_age In Milliseconds | ||
* @param {...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)} args | ||
* @returns {Promise<T>} | ||
*/ | ||
expire() { | ||
this.#updated_at = 0; | ||
cached(max_age, ...args) { | ||
// Ensure max_age is a valid greater than zero number | ||
if (typeof max_age !== 'number' || max_age < 0) | ||
throw new Error('CachedLookup.cached(max_age) -> max_age must be a number that is greater than zero.'); | ||
// Retrieve a serialized Array of arguments ignoring the first argument (max_age) | ||
const serialized = Array.from(arguments).slice(1); | ||
// Attempt to resolve the cached value from the cached value record | ||
const record = this._get_cache(serialized, max_age); | ||
if (record) return Promise.resolve(record.value); | ||
// Resolve the fresh value for the provided arguments in array serialization | ||
return this._get_fresh_value(serialized); | ||
} | ||
/* CachedLookup Getters */ | ||
/** | ||
* Returns most recently cached value for this lookup instance. | ||
* Returns a fresh value and automatically updates the internal cache with this value for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)} args | ||
* @returns {Promise<T>} | ||
*/ | ||
get value() { | ||
return this.#value; | ||
fresh(...args) { | ||
// Serialize the arguments into an Array | ||
const serialized = Array.from(arguments); | ||
// Resolve the fresh value for the provided serialized arguments | ||
return this._get_fresh_value(serialized); | ||
} | ||
/** | ||
* Returns the milliseconds unix timestamp of the last cached value update. | ||
* @returns {Number} | ||
* Expires the cached value for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)} args | ||
* @returns {Boolean} True if the cache value was expired, false otherwise. | ||
*/ | ||
get updated_at() { | ||
return this.#updated_at; | ||
expire(...args) { | ||
// Retrieve the identifier string for the provided arguments | ||
const identifier = this._arguments_to_identifier(Array.from(arguments)); | ||
// Remove the cached value record for the specified arguments | ||
if (this.#cache[identifier]) { | ||
delete this.#cache[identifier]; | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Returns whether this instance is currently fetching a fresh value. | ||
* Returns whether a fresh value is currently being resolved for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)} args | ||
* @returns {Boolean} | ||
*/ | ||
get in_flight() { | ||
return this.#promise instanceof Promise; | ||
in_flight(...args) { | ||
// Retrieve the identifier string for the provided arguments | ||
const identifier = this._arguments_to_identifier(Array.from(arguments)); | ||
// Return true if there is a promise for the specified arguments | ||
return this.#promises[identifier] !== undefined; | ||
} | ||
/* CachedLookup Getters */ | ||
/** | ||
* Returns the underlying cache object which contains the cached values identified by their serialized arguments. | ||
* | ||
* @returns {Map<String, ValueRecord>} | ||
*/ | ||
get cache() { | ||
return this.#cache; | ||
} | ||
} | ||
module.exports = CachedLookup; |
{ | ||
"name": "cached-lookup", | ||
"version": "3.0.3", | ||
"version": "4.0.0", | ||
"description": "A Simple Package To Cache And Save On Expensive Lookups & Operations.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -20,4 +20,4 @@ # CachedLookup: A Simple Package To Cache And Save On Expensive Lookups & Operations. | ||
- TypeScript Support | ||
- Asynchronous By Nature | ||
- Customizable Cache Lifetime | ||
- Parameter Based Caching | ||
- Dynamic Cache Consumption | ||
@@ -39,20 +39,27 @@ - Extremely Lightweight | ||
// Create an instance that caches for 5 seconds | ||
// This will ensure, new data is only fetched every 5 seconds | ||
const CurrencyLookup = new CachedLookup(async () => { | ||
// Hit some third-party API to retrieve fresh currency data | ||
const result = await get_currency_values(); | ||
// Create a cached lookup instance which fetches music concerts from different cities on a specific date | ||
const ConcertsLookup = new CachedLookup(async (city) => { | ||
// Assume that the function get_city_concerts() is calling a Third-Party API which has a rate limit | ||
const concerts = await get_city_concerts(city); | ||
// Perform some local parsing of the resolved data | ||
const parsed = parse_data(result); | ||
// Return parsed data for cached-lookup to cache and serve instantly for the next 5 seconds | ||
return parsed; | ||
// Simply return the data and CachedLookup will handle the rest | ||
return concerts; | ||
}); | ||
// Some webserver route utilizing the CachedLookup instance to serve currency data | ||
webserver.get('/api/currency', async (request, response) => { | ||
// This will return the cached value for 5 seconds before retrieving a fresh value | ||
const data = await CurrencyLookup.cached(5000); | ||
return response.send(data); | ||
// Create some route which serves this data with a 10 second intermittent cache | ||
webserver.get('/api/concerts/:city', async (request, response) => { | ||
// Retrieve the city value from the request - assume there is user validation done on this here | ||
const city = request.path_parameters.city; | ||
// Retrieve data from the CachedLookup with the cached() and pass the city in the call to the lookup handler | ||
// Be sure to specify the first parameter as the max_age of the cached value in milliseconds | ||
// In our case, 10 seconds would be 10,000 milliseconds | ||
const concerts = await ConcertsLookup.cached(1000 * 10, city); | ||
// Simply return the data to the user | ||
// Because we retrieved this data from the ConcertsLookup with the cached() method | ||
// We can safely assume that we will only perform up to 1 Third-Party API request per city every 10 seconds | ||
return response.json({ | ||
concerts | ||
}); | ||
}); | ||
@@ -66,5 +73,6 @@ ``` | ||
* `new CachedLookup(Function: lookup)`: Creates a new CachedLookup instance. | ||
* `lookup` [`Function`]: Lookup handler which is invocated to get fresh results. | ||
* `lookup` [`Function`]: Lookup handler which is called to get fresh values. | ||
* **Note!** this callback can be either `synchronous` or `asynchronous`. | ||
* **Note!** you must return/resolve a value through this callback for the caching to work properly. | ||
* **Note!** arguments passed to the methods below will be available in each call to this handler. | ||
@@ -74,16 +82,20 @@ #### CachedLookup Properties | ||
| :-------- | :------- | :------------------------- | | ||
| `value` | `Any` | Most recent cached value | | ||
| `updated_at` | `Number` | Unix timestamp in milliseconds of latest update | | ||
| `in_flight` | `Boolean` | Whether instance is currently looking up a fresh value | | ||
| `cached` | `Object` | Internal cache store used to cache values. | | ||
#### CachedLookup Methods | ||
* `cached(Number: max_age)`: Consumes cached value with a fallback to fresh value if cached value has expired. | ||
* **Returns** a `Promise` which is then resolved to the lookup value. | ||
* `cached(Number: max_age, ...arguments)`: Returns the cached value for the provided set of arguments from the lookup handler. | ||
* **Returns** a `Promise` which is then resolved to the lookup resolved value. | ||
* **Note** parameter `max_age` should be a `Number` in `milliseconds` to specify the maximum acceptable cache age. | ||
* **Note** this method will reject when the lookup handler also rejects. | ||
* `fresh()`: Retrieves fresh value from the lookup handler. | ||
* **Returns** a `Promise` which is then resolved to the lookup value. | ||
* `expire()`: Expires current cached value marking instance to fetch fresh value on next `cached()` invocation. | ||
* **Note** this method will automatically fall back to a `fresh()` call if no viable cache value is available. | ||
* **Note** the returned `Promise` will **reject** when the lookup handler also rejects. | ||
* **Note** the provided `arguments` in after the `max_age` will be available inside of the `lookup` handler function. | ||
* `fresh(...arguments)`: Retrieves the fresh value for the provided set of arguments from the lookup handler. | ||
* **Returns** a `Promise` which is then resolved to the lookup resolved value. | ||
* `expire(...arguments)`: Expires the cached value (If one exists) for the provided set of arguments. | ||
* **Returns** a `Boolean` which specifies whether a cache value was expired or not. | ||
* `in_flight(...arguments)`: Checks whether a fresh value is currently being resolved for the provided set of arguments. | ||
* **Returns** a `Boolean` to specify the result. | ||
* **Note** the `...arguments` are **optional** for all methods above and you may call each function without any arguments to a single global value. | ||
## License | ||
[MIT](./LICENSE) |
type LookupHandler<T extends any> = () => T | Promise<T> | ||
type SupportedTypes = string | number | boolean; | ||
type SupportedArgumentTypes = SupportedTypes | SupportedTypes[]; | ||
interface ValueRecord { | ||
value: T, | ||
updated_at: number | ||
}; | ||
export default class CachedLookup<T extends any> { | ||
/** | ||
* Constructs a cached lookup with the specified lookup handler. | ||
* | ||
* @param lookup | ||
* Creates a new CachedLookup instance with the specified lookup function. | ||
* The lookup function can be both synchronous or asynchronous. | ||
* | ||
* @param {function(...(SupportedArgumentTypes|Array<SupportedArgumentTypes>)):T} lookup | ||
*/ | ||
@@ -13,39 +20,43 @@ constructor(lookup: LookupHandler<T>) | ||
/** | ||
* Returns cached value if the cached value is not older than the specified maximum age in milliseconds. | ||
* This method automatically retrieves a fresh value if the cached value is older than the specified maximum age. | ||
* | ||
* Returns a cached value that is up to max_age milliseconds old for the provided set of arguments. | ||
* Falls back to a fresh value if the cache value is older than max_age. | ||
* | ||
* @param {Number} max_age In Milliseconds | ||
* @returns {Promise} | ||
* @param {...(SupportedArgumentTypes)} args | ||
* @returns {Promise<T>} | ||
*/ | ||
cached(max_age: number): Promise<T>; | ||
cached(max_age: number, ...args: SupportedArgumentTypes): Promise<T>; | ||
/** | ||
* Fetches a fresh value from the lookup handler and returns result. | ||
* @returns {Promise} | ||
* Returns a fresh value and automatically updates the internal cache with this value for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes)} args | ||
* @returns {Promise<T>} | ||
*/ | ||
fresh(): Promise<T>; | ||
fresh(...args: SupportedArgumentTypes): Promise<T>; | ||
/** | ||
* Expires the current cached value marking the instance to retrieve a fresh value on next call. | ||
* Expires the cached value for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes)} args | ||
* @returns {Boolean} True if the cache value was expired, false otherwise. | ||
*/ | ||
expire(): void; | ||
expire(...args: SupportedArgumentTypes): bool; | ||
/* CachedLookup Getters */ | ||
/** | ||
* Returns most recently cached value for this lookup instance. | ||
* Returns whether a fresh value is currently being resolved for the provided set of arguments. | ||
* | ||
* @param {...(SupportedArgumentTypes)} args | ||
* @returns {Boolean} | ||
*/ | ||
get value(): T; | ||
in_flight(...args: SupportedArgumentTypes): bool; | ||
/** | ||
* Returns the milliseconds unix timestamp of the last cached value update. | ||
* @returns {Number} | ||
*/ | ||
get updated_at(): number; | ||
/* CachedLookup Getters */ | ||
/** | ||
* Returns whether this instance is currently fetching a fresh value. | ||
* @returns {Boolean} | ||
* Returns the underlying cache object which contains the cached values identified by their serialized arguments. | ||
* | ||
* @returns {Map<String, ValueRecord>} | ||
*/ | ||
get in_flight(): boolean; | ||
get cache(): Map<string, ValueRecord>; | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
16756
227
98
1