reuse-promise
Purpose
TL;DR - Prevent from a unique async process (function that returns a promise) to run more than once concurrently by temporarily caching the promise until it's resolved/rejected.
When a function returns a promise and it's being called from multiple places in the app, new promises are being instantiated, and multiple async operations are going to be executed.
A common case is a function that gets an articleId
and returns a promise that calls API. This function can be called from multiple places, each time will create a new promise and will issue a new request. This is usually not desired:
function findArticle(articleId) {
return fetch(`/article/${articleId}`).then(r => r.json())
// could also be
// return new Promise(...)
}
// will issue first request for articleId=1
findArticle(1).then(article1 => console.log(article1))
// will issue second request for articleId=1
findArticle(1).then(article1 => console.log(article1))
// will issue first request for articleId=2
findArticle(2).then(article2 => console.log(article2))
reuse-promise
decorates a function and temporary memoizes a promise until it's resolved. In this case, the first call for articleId=1
will create the new promise, issue the HTTP request, and remember that created promise for articleId=1
. The second call with the same argument will return the same promise from earlier call. However, once the original promise is resolved (or rejected), a new call to findArticle(1)
will issue a new request.
An initial call to a wrapped function goes through the original function, and then indexes the returned promise by a json-serialized string of the arguments that were sent to the function. So findArticles([1, 2, 3])
can be called twice and still return the same promise, becasue JSON.stringify([1, 2, 3]) === JSON.stringify([1, 2, 3])
.
Installation
npm install reuse-promise --save
Usage
reuse-promise
can be used as a decorator in a class definition or as a wrapper to a function.
As a class decorator
Requires babel
and babel-plugin-transform-decorators-legacy
plugin.
import { decorator as reusePromise } from 'reuse-promise'
class ArticleService {
@reusePromise()
find(articleId) {
return fetch(`/article/${articleId}`).then(r => r.json())
}
}
const articleService = new ArticleService()
// will issue first request for articleId=1
articleService.find(1).then(article1 => console.log(article1))
// WILL NOT issue any request for articleId=1, will reuse the promise that was created in previous call
articleService.find(1).then(article1 => console.log(article1))
// will issue first request for articleId=2
articleService.find(2).then(article2 => console.log(article2))
Wrapping a function
import reusePromise from 'reuse-promise'
function findArticle(articleId) {
return fetch(`/article/${articleId}`).then(r => r.json())
}
const findArticleReusedPromise = reusePromise(findArticle/*, options */)
// will issue first request for articleId=1
findArticleReusedPromise(1).then(article1 => console.log(article1))
// WILL NOT issue any request for articleId=1, will reuse the promise that was created in previous call
findArticleReusedPromise(1).then(article1 => console.log(article1))
// will issue first request for articleId=2
findArticleReusedPromise(2).then(article2 => console.log(article2))
memoize
option: reuse-promise
can indefinitely remember the value that was returned from a promise, so no async code will execute more than once, even if the promise was previously resolved:
import { decorator as reusePromise } from 'reuse-promise'
class ArticleService {
@reusePromise({ memoize: true })
find(articleId) {
return fetch(`/article/${articleId}`).then(r => r.json())
}
}
const articleService = new ArticleService()
articleService.find(1).then(article1 => console.log(article1))
setTimeout(() => {
// here, the original promise is resolved
// without memoize: true, calling find(1) would go through original function and create a promise
// however, with memoize the following call will be immediately resolved with the value
articleService.find(1).then(article1 => console.log(article1))
}, 1000)
Clearing all memoized values of a function can be done with:
reusePromise.clear(articleService.find)
// or
articleService.find.__reusePromise__clear()
Clear all:
reusePromise.clear()
serializeArguments
option: By default, reuse-promise
indexes promises in a dictionarty where the key is all arguments JSON.stringify
ied. This is sometimes an unnecessary process, especially when sending big objects as arguments.
A custom argument serializer can be provided. To reuse promises based on the first letter of the first argument, for example, provide:
@reusePromise({
serializeArguments: args => args[0][0]
})
Or, to grab an ID of a given model without having it all serialized:
updateUserName = reusePromise(updateUserName, {
serializeArguments: args => args[0].id
})
const someUser = { id: 1, name: 'name' }
updateUserName(someUser, 'new name')
Test
npm install
npm test
License
MIT