lilypads
v1.3.2
Published
Memoized functions done right
Downloads
32
Readme
lilypads
memoized functions done right
npm install lilypads
Why, and what it does
I found myself writing a lot of optimised handler functions that were repeating a lot of the optimization techniques over and over again. This module does all of that for you.
Provided a unique id
, lilypads
will, after the first call, ensure immediate function responses.
Upon an initial first call with the id
user-34134-data
, it will get the data from the provided responder
function. However, next time the same request is made, lilypads
will naively and immediately send the same result as it got from the responder
function earlier.
In addition to this, lilypads
has @amphibian/party
built in, which ensures multiple calls to what would give the same response only trigger the responder
function once.
Usage
import lilypads from 'lilypads';
import {slowGetUserData} from './interfaces/user';
export default function optimizedGetUserData(userId) {
return lilypads({
id: `optimizedGetUserData/${userId}`,
lifetime: 5 * 60 * 1000 // 5 minutes
}, () => getUserData(userId));
}
Step by step
Assume a call to id
user-34134-data
:
First call
- Calls the
responder
function. - Returns the response.
Second call
Immediately returns the previous result of the responder
function.
Fourth call, assuming provided lifetime
has expired
- Immediately returns the previous result.
- In the background, calls the
responder
function and swaps the current result with a new one.
A note on error handling
If the lilypads
responder
encounters an error the first time it runs, it will throw an error. However, if it has already been run successfully, lilypads
will swallow the error and send it to the optional errorHandler
you can provide.
Consider the following code:
let shouldError = false;
function slowGetUserData(userId) {
if (shouldError) {
throw new Error();
}
shouldError = true;
return {user: 'test'};
}
function optimizedGetUserData(userId) {
return lilypads(context, {
id: `optimizedGetUserData/${userId}`
}, () => getUserData(userId));
}
(async () => {
await optimizedGetUserData('1');
await optimizedGetUserData('1');
})();
No errors will be thrown because the responder
function has already had a successful run. The error can be handled by implementing an errorHandler
:
// ...
await lilypads(context, {
id: `optimizedGetUserData/${userId}`
}, () => (
getUserData(userId)
), (error) => {
console.error('This error happened:', error);
});
// ...
However, if the error is thrown before the responder
has been run once, successfully, the error is thrown “as normal”:
// ...
try {
await lilypads(context, {
id: `optimizedGetUserData/${userId}`
}, () => (
getUserData(userId)
), (error) => {
console.error('This error happened:', error);
});
} catch (error) {
console.error('This error happened:', error);
}
// ...
To ensure an error is always thrown, use lilypads.ForceThrowError
:
let shouldError = false;
function slowGetUserData(userId) {
if (shouldError) {
throw new lilypads.ForceThrowError();
}
shouldError = true;
return {user: 'test'};
}
function optimizedGetUserData(userId) {
return lilypads(context, {
id: `optimizedGetUserData/${userId}`
}, () => getUserData(userId));
}
(async () => {
await optimizedGetUserData('1');
await optimizedGetUserData('1');
})();
This time, an error will be thrown, even if the previous responder
function had a successful run. Both of these approaches will work:
throw new lilypads.ForceThrowError();
throw new lilypads.ForceThrowError(new Error('my error'));
A note on cache invalidation
Sometimes you make changes in your, ie., database that you would like to reflect immediately. There's an option to force update a lilypad
in the options
object: forceUpdate
.
It should be set to either sync
or async
depending on the desired effect. If you make a change that does not need immediate reflection, use async
. If not, use sync
.
// ...
function getUser(userId, options) {
return lilypads({
...options,
id: `getUser/${userId}`
}, () => getUserDataFromDatabase(userId));
}
function updateUser(userId) {
await updateUserInDatabase(userId, {email: 'test@bazinga.com'});
return getUser(userId, {forceUpdate: 'sync'});
}
// ...
forceUpdate
should only be set on the lilypad
call when you know there's been a change. You could also implement some invalidation logic to be evaluated on runtime:
// ...
import invalidate, {stale} from '../my-utilities/invalidations';
function getUser(userId, options) {
if (stale(`my-invalidation-logic/${userId}`)) {
options.forceUpdate = 'sync';
}
return lilypads({
...options,
id: `getUser/${userId}`
}, () => getUserDataFromDatabase(userId));
}
async function updateUser(userId) {
await updateUserInDatabase(userId, {email: 'test@bazinga.com'});
invalidate(`my-invalidation-logic/${userId}`);
return getUser(userId);
}
// ...
lilypads
Usage
lilypads(options, responder);
options
(Object
) Required.
options.id
(String
) Required.
Should be unique, yet the same for requests that expect the same response. Function arguments used within responder
should probably be represented here in some way. For example:
user/34134
my-blog/article/213
options.lifetime
(Number
)
How long each responder
result will live in milliseconds. If undefined
, the result lives forever (or until forceUpdate
is set). If set to, eg., 3000
, leap
will get a new version after 3000
ms. But it won't throw out the old one until the new one is ready.
options.forceUpdate
(String
): sync
|async
To force update the lilypad
, set forceUpdate
to either sync
or async
. This will ensure the responder
function is called to update the cached return value.
You have two choices:
sync
The lilypad
will call the responder
function and resolve upon its completion. This is useful when the change made needs to be reflected immediately.
async
The lilypad
will resolve immediately, as normal, returning an “old” responder
result (if any) – but will, in the background, call the responder
function to update the lilypad
.
responder
(Function
)
The function that returns the request response. It is given no arguments when called. Can return a Promise
.
errorHandler
(Function
)
The function that is given any error encountered running the responder
function.
Returns lilypad
The response.