Created
March 11, 2018 09:07
-
-
Save justsml/e5b1bf5e50c39208eebf5bd9dc658abc to your computer and use it in GitHub Desktop.
Debounce Promise Results using Naïve Timeout-based Expiration/Caching
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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