Skip to content

Instantly share code, notes, and snippets.

@bathos
Last active November 8, 2017 18:59
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 bathos/431194408c70b19cfbc3e0819ad0db40 to your computer and use it in GitHub Desktop.
Save bathos/431194408c70b19cfbc3e0819ad0db40 to your computer and use it in GitHub Desktop.
debouncing-promises.js
// 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