-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
511 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
# @keyv/memoize [<img width="100" align="right" src="https://ghcdn.rawgit.org/microlinkhq/keyv/master/media/logo-sunset.svg" alt="keyv">](https://github.com/microlinkhq/keyv) | ||
|
||
> Memoize any function using Keyv as storage backend. | ||
## Install | ||
|
||
```shell | ||
npm install --save keyv @keyv/memoize | ||
``` | ||
|
||
## Usage | ||
|
||
```js | ||
const memoize = require('@keyvhq/memoize'); | ||
|
||
const memoizedRequest = memoize(request); | ||
|
||
memoizedRequest('http://example.com').then(resp => { /* from request */ }); | ||
memoizedRequest('http://example.com').then(resp => { /* from cache */ }); | ||
``` | ||
|
||
You can pass a [keyv](https://github.com/microlinkhq/keyv) instance or options to be used as argument. | ||
|
||
```js | ||
memoize(request, { store: new Map() }); | ||
memoize(request, 'redis://user:pass@localhost:6379'); | ||
memoize(request, new Keyv()); | ||
``` | ||
|
||
### Resolver | ||
|
||
By default the first argument of your function call is used as cache key. | ||
|
||
You can use a resolver if you want to change the key. The resolver is called with the same arguments as the function. | ||
|
||
```js | ||
const sum = (n1, n2) => n1 + n2; | ||
|
||
const memoized = memoize(sum, new Keyv(), { | ||
resolver: (n1, n2) => `${n1}+${n2}` | ||
}); | ||
|
||
// cached as { '1+2': 3 } | ||
memoized(1, 2); | ||
``` | ||
|
||
The library uses flood protection internally based on the result of this resolver. This means you can make as many requests as you want simultaneously while being sure you won't flood your async resource. | ||
|
||
### TTL | ||
|
||
Set `ttl` to a `number` for a static TTL value. | ||
|
||
```js | ||
const memoizedRequest = memoize(request, new Keyv(), { ttl: 60000 }); | ||
|
||
// cached for 60 seconds | ||
memoizedRequest('http://example.com'); | ||
``` | ||
|
||
Set `ttl` to a `function` for a dynamic TTL value. | ||
|
||
```js | ||
const memoizedRequest = memoize(request, new Keyv(), { | ||
ttl: (res) => res.statusCode === 200 ? 60000 : 0 | ||
}); | ||
|
||
// cached for 60 seconds only if response was 200 OK | ||
memoizedRequest('http://example.com'); | ||
``` | ||
|
||
### Stale | ||
|
||
Set `stale` to any `number` of milliseconds. | ||
|
||
If the `ttl` of a requested resource is below this staleness threshold we will still return the stale value but meanwhile asynchronously refresh the value. | ||
|
||
```js | ||
const memoizedRequest = memoize(request, new Keyv(), { | ||
ttl: 60000, | ||
stale: 10000 | ||
}); | ||
|
||
// cached for 60 seconds | ||
memoizedRequest('http://example.com'); | ||
|
||
// … 55 seconds later | ||
// Our cache will expire in 5 seconds. | ||
// This is below the staleness threshold of 10 seconds. | ||
// returns cached result + refresh cache on background | ||
memoizedRequest('http://example.com'); | ||
``` | ||
|
||
When the `stale` option is set we won't delete expired items either. The same logic as above applies. | ||
|
||
## API | ||
|
||
### memoize(fn, \[keyvOptions], \[options]) | ||
|
||
#### fn | ||
|
||
Type: `Function`<br> | ||
*Required* | ||
|
||
Promise-returning or async function to be memoized. | ||
|
||
#### keyvOptions | ||
|
||
Type: `Object` | ||
|
||
The [Keyv]https://github.com/microlinkhq/keyv] instance or [keyv#options](https://github.com/microlinkhq/keyv#options) to be used. | ||
|
||
#### options | ||
|
||
##### resolver | ||
|
||
Type: `Function`<br/> | ||
Default: `identity` | ||
|
||
##### ttl | ||
|
||
Type: `Number` or `Function`<br/> | ||
Default: `undefined` | ||
|
||
The time-to-live quantity of time the value will considered as fresh. | ||
|
||
##### stale | ||
|
||
Type: `Number`<br/> | ||
Default: `undefined` | ||
|
||
The staleness threshold we will still return the stale value but meanwhile asynchronously refresh the value. | ||
|
||
## License | ||
|
||
**@keyvhq/memoize** © [Microlink](https://microlink.io), Released under the [MIT](https://github.com/microlinkhq/keyv/blob/master/LICENSE.md) License.<br/> | ||
Authored and maintained by [Microlink](https://microlink.io) with help from [contributors](https://github.com/microlinkhq/keyv/contributors). | ||
|
||
> [microlink.io](https://microlink.io) · GitHub [@MicrolinkHQ](https://github.com/microlinkhq) · Twitter [@microlinkhq](https://twitter.com/microlinkhq) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "@keyvhq/memoize", | ||
"description": "Memoize any function using Keyv as storage backend.", | ||
"homepage": "https://keyv.js.org", | ||
"version": "1.0.2", | ||
"main": "src/index.js", | ||
"author": { | ||
"email": "hello@microlink.io", | ||
"name": "microlink.io", | ||
"url": "https://microlink.io" | ||
}, | ||
"repository": { | ||
"directory": "packages/memo", | ||
"type": "git", | ||
"url": "git+https://github.com/microlinkhq/keyv.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/microlinkhq/keyv/issues" | ||
}, | ||
"keywords": [ | ||
"cache", | ||
"key", | ||
"memo", | ||
"memoize", | ||
"store", | ||
"ttl", | ||
"value" | ||
], | ||
"dependencies": { | ||
"@keyvhq/core": "^1.0.2", | ||
"json-buffer": "^3.0.0", | ||
"mimic-fn": "~3.0.0", | ||
"p-any": "~2.1.0" | ||
}, | ||
"devDependencies": { | ||
"ava": "latest", | ||
"delay": "~5.0.0", | ||
"nyc": "latest", | ||
"p-event": "~4.2.0" | ||
}, | ||
"engines": { | ||
"node": ">= 12" | ||
}, | ||
"files": [ | ||
"src" | ||
], | ||
"scripts": { | ||
"test": "nyc --temp-dir ../../.nyc_output ava" | ||
}, | ||
"license": "MIT" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
'use strict' | ||
|
||
const Keyv = require('@keyvhq/core') | ||
const mimicFn = require('mimic-fn') | ||
const pAny = require('p-any') | ||
|
||
const identity = value => value | ||
|
||
function memoize ( | ||
fn, | ||
keyvOptions, | ||
{ resolver = identity, ttl: rawTtl, stale: rawStale } = {} | ||
) { | ||
const keyv = keyvOptions instanceof Keyv ? keyvOptions : new Keyv(keyvOptions) | ||
const pending = {} | ||
const ttl = typeof rawTtl === 'function' ? rawTtl : () => rawTtl | ||
const stale = typeof rawStale === 'number' ? rawStale : undefined | ||
|
||
/** | ||
* This can be better. Check: | ||
* - https://github.com/lukechilds/keyv/issues/36 | ||
* | ||
* @param {string} key | ||
* @return {Promise<object>} { expires:number, value:* } | ||
*/ | ||
async function getRaw (key) { | ||
const raw = await keyv.store.get(keyv._getKeyPrefix(key)) | ||
return typeof raw === 'string' ? keyv.deserialize(raw) : raw | ||
} | ||
|
||
/** | ||
* @param {string} key | ||
* @return {Promise<*>} value | ||
* @throws if not found | ||
*/ | ||
function getStoredValue (key) { | ||
return getRaw(key).then(data => { | ||
if (!data || data.value === undefined) { | ||
throw new Error('Not found') | ||
} | ||
return data.value | ||
}) | ||
} | ||
|
||
/** | ||
* @param {string} key | ||
* @param {*[]} args | ||
* @return {Promise<*>} value | ||
*/ | ||
async function refreshValue (key, args) { | ||
return updateStoredValue(key, await fn(...args)) | ||
} | ||
|
||
/** | ||
* @param {string} key | ||
* @param {*} value | ||
* @return {Promise} resolves when updated | ||
*/ | ||
async function updateStoredValue (key, value) { | ||
await keyv.set(key, value, ttl(value)) | ||
return value | ||
} | ||
|
||
/** | ||
* @return {Promise<*>} | ||
*/ | ||
function memoized (...args) { | ||
const key = resolver(...args) | ||
|
||
if (pending[key] !== undefined) { | ||
return pAny([getStoredValue(key), pending[key]]) | ||
} | ||
|
||
pending[key] = getRaw(key).then(async data => { | ||
const hasValue = data ? data.value !== undefined : false | ||
const hasExpires = hasValue && typeof data.expires === 'number' | ||
const ttlValue = hasExpires ? data.expires - Date.now() : undefined | ||
const isExpired = stale === undefined && hasExpires && ttlValue < 0 | ||
const isStale = stale !== undefined && hasExpires && ttlValue < stale | ||
|
||
if (hasValue && !isExpired && !isStale) { | ||
pending[key] = undefined | ||
return data.value | ||
} | ||
|
||
if (isExpired) keyv.delete(key) | ||
const promise = refreshValue(key, args) | ||
|
||
if (isStale) { | ||
promise | ||
.then(value => keyv.emit('memoize.fresh.value', value)) | ||
.catch(error => keyv.emit('memoize.fresh.error', error)) | ||
return data.value | ||
} | ||
|
||
try { | ||
const value = await promise | ||
pending[key] = undefined | ||
return value | ||
} catch (error) { | ||
pending[key] = undefined | ||
throw error | ||
} | ||
}) | ||
|
||
return pending[key] | ||
} | ||
|
||
mimicFn(memoized, fn) | ||
|
||
return Object.assign(memoized, { keyv, resolver, ttl }) | ||
} | ||
|
||
module.exports = memoize |
Oops, something went wrong.