Last active
November 8, 2017 18:59
-
-
Save bathos/431194408c70b19cfbc3e0819ad0db40 to your computer and use it in GitHub Desktop.
debouncing-promises.js
This file contains 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
// Typically "debouncing" means limiting execution frequency by a specific | |
// interval while still guaranteeing that, after a call, the behavior will still | |
// execute (just not synchronously; the relationship is many->one for "calls" to | |
// "executions"). However it is sometimes desireable to not limit by a specific | |
// time — you may just want to prevent something from recurring concurrently, | |
// without concern for overall frequency. In these cases, you may instead want | |
// to debounce "against" promises. This function accepts a method that returns a | |
// promise and returns a new function that will execute that method immediately | |
// only when the last execution’s promise remains unresolved, and otherwise will | |
// queue for re-execution (once) when the current execution is complete. In all | |
// cases it returns a promise that resolves when "that" execution is complete. | |
// | |
// It is a given in a model like this that we must execute on the "leading edge" | |
// (which is not always true for debouncing based on intervals). | |
// | |
// The treatment of arguments in debounced functions has no single right answer | |
// and depends on circumstance. In the majority of cases a debounced function | |
// either takes no arguments or should always receive the "latest" arguments. It | |
// is this behavior which is implemented here, though always-receive-first may | |
// be needed in exotic cases. | |
// | |
// Symbol-keyed properties are used because this provides a means to associate | |
// state with the method in relation to a particular calling context. A WeakMap | |
// would also be viable (though both have limitations, e.g. with frozen objects | |
// in the former case and proxies in the latter). You would not want the results | |
// to "leak" if the method was called against another context. However, for | |
// usage in a functional idiom this feature is unimportant and it would make | |
// more sense to hold state in a closure. | |
const debouncePromises = method => { | |
const currentPromise = Symbol(`ACTIVE PROMISE OF ${ method.name } METHOD`); | |
const pendingPromise = Symbol(`QUEUED PROMISE OF ${ method.name } METHOD`); | |
const pendingArguments = Symbol(`QUEUED ARGS OF ${ method.name } METHOD`); | |
const { [method.name]: debouncedMethod } = { | |
[method.name]() { | |
if (this[pendingPromise]) { | |
this[pendingArguments] = arguments; | |
return this[pendingPromise]; | |
} | |
if (this[currentPromise]) { | |
const doItAgain = () => | |
debouncedMethod.apply(this, this[pendingArguments]); | |
this[pendingArguments] = arguments; | |
this[pendingPromise] = this[currentPromise].then(doItAgain, doItAgain); | |
return this[pendingPromise]; | |
} | |
const afterCurrentPromise = () => { | |
this[currentPromise] = undefined; | |
this[pendingPromise] = undefined; | |
}; | |
try { | |
this[currentPromise] = Promise.resolve(method.apply(this, arguments)); | |
this[currentPromise].then(afterCurrentPromise, afterCurrentPromise); | |
return this[currentPromise]; | |
} catch (err) { | |
return Promise.reject(err); | |
} | |
} | |
}; | |
return debouncedMethod; | |
}; | |
// Usage: | |
// | |
// class Foo { | |
// bar(baz) { | |
// return new Promise(fulfill => { | |
// setTimeout(() => (console.log({ baz }), fulfill()), 100); | |
// }); | |
// } | |
// } | |
// | |
// Object.defineProperty(Foo.prototype, 'bar', { | |
// value: debouncePromises(Foo.prototype.bar) | |
// }); | |
// | |
// const foo = new Foo; | |
// | |
// foo.bar(1); | |
// foo.bar(2); | |
// foo.bar(3); | |
// | |
// setTimeout(() => foo.bar(4), 200); | |
// | |
// (should log bazzes 1, 3, and 4) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment