Caching values with (distributed) locking. Easily extensible with your own storage and locking implementations
npm install @scienta/locking-cacheThis library combines locking with caching and makes sure only one process computing
the same cacheable value will run at the same time.
ts
import { LockingCache } from "@scienta/locking-cache";// Expensive (async) function requesting an access token
const requestTokenMock = () => {
// Fetch api access token (async), cache for 10 minutes.
//
// Calling this multiple times in parallel will only run it once.
// the cache key is based on the function name and arguments.
return Promise.resolve({
value: 'fea80f2db003d4ebc4536023814aa885', //access token
expiresInSec: 60 * 10 //10 minutes
});
}
const cache = new LockingCache();
cache.getValue(clientId, requestTokenMock).then(result => {
// Use cached token for requesting resources
});
`$3
`ts
import {IoRedisStorage, LockingCache, RedisLocker} from "@scienta/locking-cache";
import * as IORedis from "ioredis";
import * as Redlock from "redlock";const ioRedis = new IORedis();
const cache = new LockingCache(
new IoRedisStorage(ioRedis, { namespace: 'cache' }), // cache storage in redis
new RedisLocker(new Redlock(ioRedis)) // distributed locking in redis
);
const result = await cache.getValue(clientId, async () => {
return {
value: 'fea80f2db003d4ebc4536023814aa885', //access token
expiresInSec: 60 * 10 //10 minutes
};
});
`Flow of execution
1. returns value if cached
2. acquires a lock, using the cacheKey
3. returns cached value if it was computed while waiting for a lock
4. compute and cache the value
5. release the lock
6. return the valueErrors
There are a number of errors that can come up when caching:`ts
export enum CacheErrorEvents {
resolveError = 'resolveError',
storeGetError = 'storeGetError',
storeSetError = 'storeSetError',
lockError = 'lockError',
unlockError = 'unlockError'
}
`If an error occurs, most of the time it will result in a rejected promise from
getValue():`ts
const cache = new LockingCache();
try {
const result = await cache.getValue(clientId, requestTokenMock);
} catch (error) {
// handle resolveError|storeGetError|storeSetError(|lockError)
}
`The
unlockError is an exception (it does not get thrown) and the lockError can be thrown optionally
by setting cacheOnLockError of the LockingCache to false;Still there is a way to listen to errors, because the
LockingCache also emits them:
`ts
const cache = new LockingCache();
cache.on('unlockError', (error) => {
//handle unlockError
});
cache.getValue(clientId, requestTokenMock).then(result => {
// Use cached token for requesting resources
}).catch((error) => {
// handle resolveError|storeGetError|storeSetError(|lockError)
});
`Contributing
If you want to contribute, please do so. A docker-compose.yml file is added to make development easy.$3
To start development within the defined container, just use docker-compose:
`bash
docker-compose up -d
docker exec -ti locking-cache bash
`$3
To quickly run tests and linting from a docker container, you can also use docker directly:
`bash
docker run -it -w="/app" -v ${PWD}:/app node:14-slim /bin/bash -c "npm install && npm run test && npm run lint"
``