Skip to content

Commit

Permalink
feat: add memoize package
Browse files Browse the repository at this point in the history
  • Loading branch information
Kikobeats committed Jul 30, 2021
1 parent 8ccbb75 commit 25e1c4f
Show file tree
Hide file tree
Showing 5 changed files with 511 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ You should also set a [`namespace`](#optionsnamespace) for your module so you ca
- [@keyvhq/postgres](/packages/postgres) – PostgreSQL storage adapter for Keyv.
- [@keyvhq/redis](/packages/redis) – Redis storage adapter for Keyv.
- [@keyvhq/sqlite](/packages/sqlite) – SQLite storage adapter for Keyv.
- [@keyvhq/memoize](/packages/memoize) – Memoize any function using Keyv as storage backend.

### Community storage adapters

Expand All @@ -160,7 +161,6 @@ You should also set a [`namespace`](#optionsnamespace) for your module so you ca
- [keyv-mssql](https://github.com/pmorgan3/keyv-mssql) - Microsoft SQL Server adapter for Keyv.
- [keyv-offline](https://github.com/Kikobeats/keyv-offline) – Adding offline capabilities for your keyv instance.
- [keyv-s3](https://github.com/microlinkhq/keyv-s3) - Amazon S3 storage adapter for Keyv.
- [memoized-keyv](https://github.com/moeriki/memoized-keyv) - Memoize using keyv as storage backend.
- [quick-lru](https://github.com/sindresorhus/quick-lru) - Simple "Least Recently Used" (LRU) cache.


Expand Down
138 changes: 138 additions & 0 deletions packages/memoize/README.md
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)
51 changes: 51 additions & 0 deletions packages/memoize/package.json
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"
}
114 changes: 114 additions & 0 deletions packages/memoize/src/index.js
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

0 comments on commit 25e1c4f

Please sign in to comment.