Skip to content

Instantly share code, notes, and snippets.

@justsml
Created March 11, 2018 09:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justsml/e5b1bf5e50c39208eebf5bd9dc658abc to your computer and use it in GitHub Desktop.
Save justsml/e5b1bf5e50c39208eebf5bd9dc658abc to your computer and use it in GitHub Desktop.
Debounce Promise Results using Naïve Timeout-based Expiration/Caching
module.exports = { createCachedPromise, cacheifyAll };
// TODO: Add Map/WeakMap cache isolation for arguments passed into cacheifyAll's methods
/**
* Extends all functions on an Object or Class with 'Cached' suffixed methods.
* Methods must return promises when called! Won't break existing functions/usage.
*
* -----
*
* #### WARNING: Be careful of side effects! Create unique cached function instances for every unique ID (e.g. userId's).
* #### WeakMap, Map or Object Hashes can help you achieve this.
*
* @example
* // before:
* axios.get('http://localhost:3000');
* // after:
* cacheifyAll(axios, {timeout: 10000});
* axios.getCached('http://localhost:3000');
* // append `Cached` to any promise-returning function
*
* @param {Object|Class} obj - Class or Object
* @param {Object} options - { timeout = 5000 }
*/
function cacheifyAll(object, { timeout = 5000 }) {
if (typeof arguments[0] !== 'object') { throw new Error('Cacheify\'s 1st arg must be an object or class') }
if (!Number.isInteger(timeout)) { throw new Error('Option `timeout` must be an integer') }
Object.getOwnPropertyNames(object)
.filter(key => typeof object[key] === 'function')
.forEach(fnName => {
if (!object[`${fnName}Cached`]) { // don't overwrite if cached method exists
object[`${fnName}Cached`] = createCachedPromise((...args) => object[fnName](...args), { timeout })
}
})
return object
}
/**
* createCachedPromise accepts a function which returns a Promise.
* It caches the result so all calls to the returned function yield the same data (until the timeout is elapsed.)
*
*
* -----
*
* #### WARNING: Be careful of side effects! Create unique cached function instances for every unique ID (e.g. userId's).
* #### WeakMap, Map or Object Hashes can help you achieve this.
*
* @param {Function} callback
* @param {Object} [options] - Options, default `{timeout: 5000}`
* @param {Number} [options.timeout] - options.timeout - default is 5000
* @returns
*/
function createCachedPromise(callback, { timeout = 5000 } = {}) {
if (typeof arguments[0] !== 'function') { throw new Error('Promise Cache Factory 1st arg must be a function') }
if (typeof arguments[1] !== 'object') { throw new Error('Promise Cache Factory 2nd arg must be an object') }
if (!Number.isInteger(timeout)) { throw new Error('Option `timeout` must be an integer') }
let timerId = null
let data = null
const reset = () => {
data = null
clearTimeout(timerId)
}
return (force = false) => {
if (force || data == null) {
timerId = setTimeout(reset, timeout);
data = Promise.resolve(callback());
data.catch(err => {
reset();
console.error('ERROR in createCachedPromise:', err);
return Promise.reject(err);
})
}
// Returns promise `data` in every code path
// It will only 'wait' to resolve after the first resolution;
// this means multiple `.then()`'s can wait on the result w/o issue.
return data;
}
}
const assert = require("assert");
const { createCachedPromise, cacheifyAll } = require("./cache");
let testEnvMultiplier = 1;
if (/^test/ui.test(process.env.NODE_ENV)) {
testEnvMultiplier = 2;
}
const createDelayedPromise = () => new Promise(yah => setTimeout(() => yah(42), 1));
describe("Cache", () => {
describe("#cacheifyAll", () => {
it("Extends Object with .*Cached() Methods", (done) => {
const dummyObject = {createDelayedPromise}
const cachedDummyObject = cacheifyAll(dummyObject, {timeout: 10})
dummyObject.createDelayedPromiseCached()
.then(answer => {
assert.equal(answer, 42);
done();
}).catch(done);
});
});
describe("#createCachedPromise", () => {
it("Caches a Promise, simple usage", (done) => {
const getCachedData = createCachedPromise(createDelayedPromise, { timeout: 8 });
getCachedData()
.then(answer => {
assert.equal(answer, 42);
done();
}).catch(done);
});
it("Caches a Promise, reloading after specified timeout", (done) => {
let count = 0;
const delayedPromiseCounterFn = () => {
count++;
return new Promise(yah => setTimeout(() => yah(42), 1));
}
const getCachedData = createCachedPromise(delayedPromiseCounterFn, { timeout: 5 });
const assertValueAndCount = (currCount) => {
return getCachedData().then(answer => {
assert.equal(answer, 42);
assert.equal(count, currCount);
}).catch(done);
}
assertValueAndCount(1)
assertValueAndCount(1)
setTimeout(() => {
assertValueAndCount(2);
done();
}, 12 * testEnvMultiplier)
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment