Resolve promise on external event - javascript

I want to resolve a promise when an event happens in a different function
const trigger = function() {}
const eventHandler = async function() {
while(true) {
await new Promise (resolve => {
}
// Code when the promise fulfills.
}
}
const cleaner = function() {
trigger()
}
cleaner()

You can do that really easily by re-assigning the trigger:
let trigger = () => {}
const eventHandler = async function() {
while(true) {
await new Promise (resolve => {
trigger = resolve
// ^^^^^^^^^^^^^^^^^
}
// Code when the promise fulfills.
}
}
const cleaner = function() {
trigger() // calls resolve to fulfill the currently waited-for promise
}
eventHandler() // start waiting
cleaner()
However, notice that this is generally regarded as a bad practice, you should start whatever leads to the external event, and especially installing the event listener, inside the new Promise executor callback, where you trivially have access to the resolve function.

Below is an example of how you might go about making a promise cancelable. We're using a helper function makeCancelable() that takes a promise, and returns a different promise and a cancel function. There's no way to just "cancel" an existing promise, especially when you don't have control over how the promise is made, but what you can do it wrap the existing promise in a new one that's behaves just like the original, but is also ready to resolve on command.
// Helper function that just waits for a timeout
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
function makeCancelable(promise) {
let resolveWrappedPromise
return {
promise: new Promise((resolve, reject) => {
resolveWrappedPromise = resolve
promise.then(resolve, reject)
}),
cancel: value => resolveWrappedPromise(value),
}
}
cancelPromise = () => {} // function used by onClick event
async function eventHandler() {
while (true) {
const { promise, cancel } = makeCancelable(wait(1000).then(() => 'Success!'))
cancelPromise = cancel
console.log(await promise)
}
}
eventHandler()
<button onclick="cancelPromise('Canceled!')">Cancel!</button>
In this code example, "Success!" will print out every second - unless you push the button to cancel the current promise. Repeated pushes of the button will delay "Success!" from being printed indefinably, because the promise was canceled and provided with the value "Canceled!" when cancellation occurred.

Use the waitFor method of the EventEmitter2 package (Live demo).
import EventEmitter2 from "eventemitter2";
const emitter = new EventEmitter2();
(async () => {
const data = await emitter.waitFor("test");
console.log("emitted", data);
})();
setTimeout(() => emitter.emit("test", 1, 2, 3), 1000);

Related

Why javascript promise chaining with delaying isn't executed without resolve?

I wanted to make the async function like after 1 second then promise1 is executed and THEN after 0.5 seconds is passed over promise1 executed then promise2 is executed. I tried this but it doesn't work. But the function works after putting resolve(). I got how this work but I am not still sure why it works.
const button = document.getElementById('button');
function painting() {
return new Promise ((res) => {
setTimeout(() => {
button.style.backgroundColor = 'green';
res();
}, 1000);
})
.then(() => turnOff());
}
function turnOff() {
return new Promise(() => {
setTimeout(() => {
button.style.backgroundColor = 'red'
}, 500)
})
}
async function startPaint() {
return await painting()
}
button.onclick = function() {startPaint()};
<button id='button' >
hello
</button>
The code below is the one that doesn't work:
const button = document.getElementById('button');
function painting() {
return new Promise ((res) => {
setTimeout(() => {
button.style.backgroundColor = 'green';
//res();
//Chaining doesn't happen without res();
}, 1000);
})
.then(() => turnOff());
}
function turnOff() {
return new Promise(() => {
setTimeout(() => {
button.style.backgroundColor = 'red'
}, 500)
})
}
async function startPaint() {
return await painting();
}
button.onclick = function() {startPaint()};
<button id='button' >
hello
</button>
You create a new promise and you never resolve it. You need to put a call to res() inside the setTimeout(). A new promise you manually create is never resolved until you call the resolving function from somewhere inside the promise constructor function. That's just how the promise constructor function works.
new Promise((resolve, reject) => {
// you must call resolve(someVal) or reject(someErr) inside this function
// somewhere or the promise will never resolve or reject
// for example
setTimeout(() => {
resolve("hello");
}, 1000);
// nothing happens automatically when this function finishes execution
}).then(val => {
console.log(val);
}).catch(err => {
console.log(err);
});
And, if your promise is never resolved or rejected, then neither the .then() or .catch() handlers are ever called. The promise will just sit in the "pending" state forever.
The code inside a promise needs to end with either resolve() or reject(), in order to be considered finished, so it can continue to the new step (what you've got inside .then()).
If neither of those calls are made (in this case, resolve()), then the promise hangs, and nothing else after it is executed. It's waiting.
With promises, you need to settle it in order to go to the next step, to say so.

Typescript check if promise body does not call `resolve`

Is there any typescript config option or some workaround to check if there is no resolve called in new Promise callback?
Assume I have a promise
new Promise(async (resolve, reject) => {
try {
// do some stuff
// but not calling resolve()
} catch (e) {
reject(e)
}
})
I want typescript to warn me that I did not call resolve(). Is it possible?
I know that I can use noUnusedParameters, but there are a couple of situations where I still need unused parameters (e.g. request inside of express.Hanlder, where I only use response, etc.)
No, that is not possible. Knowing whether code calls a certain function (resolve in this case) is just as hard as the halting problem. There is proof that no algorithm exists that can always determine this.
To illustrate, let's assume that the algorithm for determining whether a function calls resolve exists, and is made available via the function callsResolve(func). So callsResolve(func) will return true when it determines that func will call resolve (without actually running func), and false when it determines that func will not call resolve.
Now image this func:
function func() {
if (!callsResolve(func)) resolve();
}
... now we have a paradox: whatever this call of callsResolve returned, it was wrong. So for instance, if the implementation of callsResolve would have simulated an execution of func (synchronously) and determines that after a predefined timeout it should return false, the above is a demonstration of a function that calls resolve just after that timeout expired.
The closest you can get to a compile time check is to use async / await syntax.
If you don't want to use that, you could timeout your promises, though you would have to do that with each of your promise after / when you are creating them.
A solution could look like this:
export const resolveAfterDelay = (timeout: number) => new Promise((r) => setTimeout(r, timeout));
export const rejectAfterDelay = async (timeout: number) => {
return new Promise((resolve, reject) => setTimeout(() => reject(`Promise timed out as resolve was not called within ${timeout}ms`), timeout));
};
export const timeoutPromise = <T>(timeout: number) => async (p: Promise<T>): Promise<T> => {
return Promise.race([p, rejectAfterDelay(timeout)]);
};
const timeoutAfter1s = timeoutPromise(1e3);
const timeoutAfter10s = timeoutPromise(10e3);
timeoutAfter10s(resolveAfterDelay(3e3)).then(success => console.log("SUCCESS IS PRINTED")).catch(console.error); // works
timeoutAfter1s(resolveAfterDelay(3e3)).then(success => console.log("NEVER REACHED")).catch(console.error); // aborts
const neverResolvingPromise = new Promise(() => {
});
timeoutAfter1s(neverResolvingPromise).catch(console.error); // promise never resolves but it will be rejected by the timeout
It makes use of Promise.race. Basically, whatever first resoves or rejects will be returned. We want to always reject a Promise if it does not resolve in time.
You would always have to wrap your Promise on creation like
timeoutAfter10s(new Promise(...));
And you would have to adapt the timeout according to your use case.

Correct way to sequence two asynchronous operations that each return a promise javascript

I was wondering what is the proper way to call a promise after another promise resolves. I know we can use async await to create functions which will resolve a promise. I was wondering which form of handling promises is consider proper practice, or is it good practice to create generators instead? consider the following code:
const fetchSomething = () => new Promise((resolve) => {
setTimeout(() => resolve(console.log('future value')), 500);
});
const fetchSomethingElse = () => new Promise((resolve) => {
setTimeout(() => resolve(console.log('future value dueeee')), 3000);
});
const get = () => {
return fetchSomething().then(function(){
fetchSomethingElse()
});
}
get();
or
const fetchSomething = () => new Promise((resolve) => {
setTimeout(() => resolve({resolve: true}), 500);
});
const fetchSomethingElse = () => new Promise((resolve) => {
setTimeout(() => resolve({resolve: true}), 3000);
});
const get = async function() {
const fet = await fetchSomething();
const fet2 = await fetchSomethingElse();
};
get();
Either one is fine. Your choice.
In the first you're nesting .then() handlers. In the second, you're using the newer await to sequence them. More people are moving to await because it appears to be simpler code for sequencing operations (assuming you do proper error handling), though in this case, they are pretty similar in complexity, particularly with the simplification suggested below so it's really up to your own personal coding style.
What is missing in both is that get() just returned a promise so you need to use .then() and .catch() with it to get the value and catch any errors.
Also, something that is missing in the first is that you aren't returning the second promise which means the caller won't know when the second operation is done.
Your first can be simplified and fixed up like this:
const get = () => {
return fetchSomething().then(fetchSomethingElse);
}
get().then(val => {
// done here
}).catch(err => {
// error here
});
As Pointy mentioned, you don't "call a promise". You "call a function that returns a promise". Promises are objects. They are not callable.
Probably what your title could be rewritten to is: "Correct way to sequence two asynchronous operations that each return a promise".
For completeness, if your two async operations don't depend upon one another, then you don't have to manually sequence them. You can start them both and then monitor when both are done. This will sometimes get a faster end-to-end response.
You can do that using Promise.all():
const get = function() {
return Promise.all([fetchSomething(), fetchSomethingElse()]).then(results => {
// process results array and then return final value
// results[0] is result from fetchSomething, results[1] is result from fetchSomethingElse
return finalVal;
});
}
Both are fine, but you are making a common mistake in the top example (and maybe it's just because of the simplification of the code for the question). You are returning the promise from get, but you are not returning the promise from the then. This means the caller of get won't know when both promises have resolved. Consider:
const fetchSomething = () => new Promise((resolve) => {
setTimeout(() => resolve(console.log('future value')), 500);
});
const fetchSomethingElse = () => new Promise((resolve) => {
setTimeout(() => resolve(console.log('future value dueeee')), 3000);
});
const get = () => {
return fetchSomething().then(function(){
fetchSomethingElse()
});
}
// we don't when fetchSomethingElse is done
get().then(() => console.log("done"));
Also there's another option you might consider since the second promise doesn't depend on the output of the first. Call them in parallel:
const get = () => {
return Promise.all([fetchSomething(), fetchSomethingElse() ])
}
In this case one can start before the other is finished and the whole operation should be faster.
It's important to remember that in Promise-based patterns you're using functions that return Promises. Promises are passed in resolve and reject arguments (which are themselves functions). What you resolve with, is what gets exectuted in the .then() phase, and what you reject with gets exectuted in the .catch() phase.
To handle Promises in sequence, you're passing your values into the top-level function that wraps the Promise.
so...
const p1 = () => {
return new Promise((resolve,reject) => {
window.setTimeout(() => {
resolve('future value one');
},500);
});
};
const p2 = (v1) => {
return new Promise((resolve,reject) => {
window.setTimeout(() => {
const v2 = 'future value two';
resolve({v1,v2});
},500);
});
};
p1().then(p2).then(console.log);

Debounce function implemented with promises

I'm trying to implement a debounce function that works with a promise in javascript. That way, each caller can consume the result of the "debounced" function using a Promise. Here is the best I have been able to come up with so far:
function debounce(inner, ms = 0) {
let timer = null;
let promise = null;
const events = new EventEmitter(); // do I really need this?
return function (...args) {
if (timer == null) {
promise = new Promise(resolve => {
events.once('done', resolve);
});
} else {
clearTimeout(timer);
}
timer = setTimeout(() => {
events.emit('done', inner(...args));
timer = null;
}, ms);
return promise;
};
}
Ideally, I would like to implement this utility function without introducing a dependency on EventEmitter (or implementing my own basic version of EventEmitter), but I can't think of a way to do it. Any thoughts?
I found a better way to implement this with promises:
function debounce(inner, ms = 0) {
let timer = null;
let resolves = [];
return function (...args) {
// Run the function after a certain amount of time
clearTimeout(timer);
timer = setTimeout(() => {
// Get the result of the inner function, then apply it to the resolve function of
// each promise that has been created since the last time the inner function was run
let result = inner(...args);
resolves.forEach(r => r(result));
resolves = [];
}, ms);
return new Promise(r => resolves.push(r));
};
}
I still welcome suggestions, but the new implementation answers my original question about how to implement this function without a dependency on EventEmitter (or something like it).
In Chris's solution all calls will be resolved with delay between them, which is good, but sometimes we need resolve only last call.
In my implementation, only last call in interval will be resolved.
function debounce(f, interval) {
let timer = null;
return (...args) => {
clearTimeout(timer);
return new Promise((resolve) => {
timer = setTimeout(
() => resolve(f(...args)),
interval,
);
});
};
}
And the following typescript(>=4.5) implementation supports aborted features:
Support aborting promise via reject(). If we don't abort it, it cannot execute finally function.
Support custom reject abortValue.
If we catch error, we may need to determine if the error type is Aborted
/**
*
* #param f callback
* #param wait milliseconds
* #param abortValue if has abortValue, promise will reject it if
* #returns Promise
*/
export function debouncePromise<T extends (...args: any[]) => any>(
fn: T,
wait: number,
abortValue: any = undefined,
) {
let cancel = () => { };
// type Awaited<T> = T extends PromiseLike<infer U> ? U : T
type ReturnT = Awaited<ReturnType<T>>;
const wrapFunc = (...args: Parameters<T>): Promise<ReturnT> => {
cancel();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(fn(...args)), wait);
cancel = () => {
clearTimeout(timer);
if (abortValue!==undefined) {
reject(abortValue);
}
};
});
};
return wrapFunc;
}
/**
// deno run src/utils/perf.ts
function add(a: number) {
return Promise.resolve(a + 1);
}
const wrapFn= debouncePromise(add, 500, 'Aborted');
wrapFn(2).then(console.log).catch(console.log).finally(()=>console.log('final-clean')); // Aborted + final-clean
wrapFn(3).then(console.log).catch(console.log).finally(()=>console.log('final-clean')); // 4 + final_clean
Note:
I had done some memory benchmarks, huge number of pending promises won't cause memory leak. It seems that V8 engine GC will clean unused promises.
I landed here because I wanted to get the return value of the promise, but debounce in underscore.js was returning undefined instead. I ended up using lodash version with leading=true. It works for my case because I don't care if the execution is leading or trailing.
https://lodash.com/docs/4.17.4#debounce
_.debounce(somethingThatReturnsAPromise, 300, {
leading: true,
trailing: false
})
resolve one promise, cancel the others
Many implementations I've seen over-complicate the problem or have other hygiene issues. In this post we will write our own debounce. This implementation will -
have at most one promise pending at any given time (per debounced task)
stop memory leaks by properly cancelling pending promises
resolve only the latest promise
demonstrate proper behaviour with live code demos
We write debounce with its two parameters, the task to debounce, and the amount of milliseconds to delay, ms. We introduce a single local binding for its local state, t -
function debounce (task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => {
try {
t.cancel()
t = deferred()
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
We depend on a reusable deferred function, which creates a new promise that resolves in ms milliseconds. It introduces two local bindings, the promise itself, an the ability to cancel it -
function deferred (ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
click counter example
In this first example, we have a button that counts the user's clicks. The event listener is attached using debounce, so the counter is only incremented after a specified duration -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter
// event handler
function clickCounter (event) {
mycounter.value = Number(mycounter.value) + 1
}
// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>
live query example, "autocomplete"
In this second example, we have a form with a text input. Our search query is attached using debounce -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const myresult = myform.myresult
// event handler
function search (event) {
myresult.value = `Searching for: ${event.target.value}`
}
// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))
<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>
Here's my version in typescript (mostly based on Chris one), if someone need it 😉
function promiseDebounce (exec: (...args: any[]) => Promise<any>, interval: number): () => ReturnType<typeof exec> {
let handle: number | undefined;
let resolves: Array<(value?: unknown) => void> = [];
return async (...args: unknown[]) => {
clearTimeout(handle);
handle = setTimeout(
() => {
const result = exec(...args);
resolves.forEach(resolve => resolve(result));
resolves = [];
},
interval
);
return new Promise(resolve => resolves.push(resolve));
};
}
No clue what you are trying to accomplish as it vastly depends on what your needs are. Below is something somewhat generic though. Without a solid grasp of what is going on in the code below, you really might not want to use it though.
// Debounce state constructor
function debounce(f) {
this._f = f;
return this.run.bind(this)
}
// Debounce execution function
debounce.prototype.run = function() {
console.log('before check');
if (this._promise)
return this._promise;
console.log('after check');
return this._promise = this._f(arguments).then(function(r) {
console.log('clearing');
delete this._promise; // remove deletion to prevent new execution (or remove after timeout?)
return r;
}.bind(this)).catch(function(r) {
console.log('clearing after rejection');
delete this._promise; // Remove deletion here for as needed as noted above
return Promise.reject(r); // rethrow rejection
})
}
// Some function which returns a promise needing debouncing
function test(str) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('test' + str);
resolve();
}, 1000);
});
}
a = new debounce(test); // Create debounced version of function
console.log("p1: ", p1 = a(1));
console.log("p2: ", p2 = a(2));
console.log("p1 = p2", p1 === p2);
setTimeout(function() {
console.log("p3: ", p3 = a(3));
console.log("p1 = p3 ", p1 === p3, " - p2 = p3 ", p2 === p3);
}, 2100)
View the console when running the code above. I put a few messages to show a bit about what is going on. First some function which returns a promise is passed as an argument to new debounce(). This creates a debounced version of the function.
When you run the debounced function as the code above does (a(1), a(2), and a(3)) you will notice during processing it returns the same promise instead of starting a new one. Once the promise is complete it removes the old promise. In code above I wait for the timeout manually with setTimeout before running a(3).
You can clear the promise in other ways as well, like adding a reset or clear function on debounce.prototype to clear the promise at a different time. You could also set it to timeout. The tests in the console log should show p1 and p2 get the same promise (reference comparison "===" is true) and that p3 is different.
Here is what I came up with to solve this issue. All calls to the debounced function batched to the same invocation all return the same Promise that resolves to the result of the future invocation.
function makeFuture() {
let resolve;
let reject;
let promise = new Promise((d, e) => {
resolve = d;
reject = e;
});
return [promise, resolve, reject];
}
function debounceAsync(asyncFunction, delayMs) {
let timeout;
let [promise, resolve, reject] = makeFuture();
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const [prevResolve, prevReject] = [resolve, reject];
[promise, resolve, reject] = makeFuture();
try {
prevResolve(await asyncFunction.apply(this, args));
} catch (error) {
prevReject(error);
}
}, delayMs);
return promise;
}
}
const start = Date.now();
const dog = {
sound: 'woof',
bark() {
const delay = Date.now() - start;
console.log(`dog says ${this.sound} after ${delay} ms`);
return delay;
},
};
dog.bark = debounceAsync(dog.bark, 50);
Promise.all([dog.bark(), dog.bark()]).then(([delay1, delay2]) => {
console.log(`Delay1: ${delay1}, Delay2: ${delay2}`);
});
Both Chris and Николай Гордеев have good solutions. The first will resolve all of them. The problem is that they all be resolved, but usually you wouldn't want all of them to run.
The second solution solved that but created a new problem - now you will have multiple awaits. If it's a function that is called a lot (like search typing) you might have a memory issue. I fixed it by creating the following asyncDebounce that will resolve the last one and reject (and the awaiting call will get an exception that they can just catch).
const debounceWithRejection = (
inner,
ms = 0,
reject = false,
rejectionBuilder
) => {
let timer = null;
let resolves = [];
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
const resolvesLocal = resolves;
resolves = [];
if (reject) {
const resolve = resolvesLocal.pop();
resolve.res(inner(...args));
resolvesLocal.forEach((r, i) => {
!!rejectionBuilder ? r.rej(rejectionBuilder(r.args)) : r.rej(r.args);
});
} else {
resolvesLocal.forEach((r) => r.res(inner(...args)));
}
resolves = [];
}, ms);
return new Promise((res, rej) =>
resolves.push({ res, rej, args: [...args] })
);
};
};
The rejection logic is optional, and so is the rejectionBuilder. It's an option to reject with specific builder so you will know to catch it.
You can see runing example.
This may not what you want, but can provide you some clue:
/**
* Call a function asynchronously, as soon as possible. Makes
* use of HTML Promise to schedule the callback if available,
* otherwise falling back to `setTimeout` (mainly for IE<11).
* #type {(callback: function) => void}
*/
export const defer = typeof Promise=='function' ?
Promise.resolve().then.bind(Promise.resolve()) : setTimeout;

Promise - is it possible to force cancel a promise

I use ES6 Promises to manage all of my network data retrieval and there are some situations where I need to force cancel them.
Basically the scenario is such that I have a type-ahead search on the UI where the request is delegated to the backend has to carry out the search based on the partial input. While this network request (#1) may take a little bit of time, user continues to type which eventually triggers another backend call (#2)
Here #2 naturally takes precedence over #1 so I would like to cancel the Promise wrapping request #1. I already have a cache of all Promises in the data layer so I can theoretically retrieve it as I am attempting to submit a Promise for #2.
But how do I cancel Promise #1 once I retrieve it from the cache?
Could anyone suggest an approach?
In modern JavaScript - no
Promises have settled (hah) and it appears like it will never be possible to cancel a (pending) promise.
Instead, there is a cross-platform (Node, Browsers etc) cancellation primitive as part of WHATWG (a standards body that also builds HTML) called AbortController. You can use it to cancel functions that return promises rather than promises themselves:
// Take a signal parameter in the function that needs cancellation
async function somethingIWantToCancel({ signal } = {}) {
// either pass it directly to APIs that support it
// (fetch and most Node APIs do)
const response = await fetch('.../', { signal });
// return response.json;
// or if the API does not already support it -
// manually adapt your code to support signals:
const onAbort = (e) => {
// run any code relating to aborting here
};
signal.addEventListener('abort', onAbort, { once: true });
// and be sure to clean it up when the action you are performing
// is finished to avoid a leak
// ... sometime later ...
signal.removeEventListener('abort', onAbort);
}
// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
try {
await somethingIWantToCancel({ signal: ac.signal });
} catch (e) {
if (e.name === 'AbortError') {
// deal with cancellation in caller, or ignore
} else {
throw e; // don't swallow errors :)
}
}
No. We can't do that yet.
ES6 promises do not support cancellation yet. It's on its way, and its design is something a lot of people worked really hard on. Sound cancellation semantics are hard to get right and this is work in progress. There are interesting debates on the "fetch" repo, on esdiscuss and on several other repos on GH but I'd just be patient if I were you.
But, but, but.. cancellation is really important!
It is, the reality of the matter is cancellation is really an important scenario in client-side programming. The cases you describe like aborting web requests are important and they're everywhere.
So... the language screwed me!
Yeah, sorry about that. Promises had to get in first before further things were specified - so they went in without some useful stuff like .finally and .cancel - it's on its way though, to the spec through the DOM. Cancellation is not an afterthought it's just a time constraint and a more iterative approach to API design.
So what can I do?
You have several alternatives:
Use a third party library like bluebird who can move a lot faster than the spec and thus have cancellation as well as a bunch of other goodies - this is what large companies like WhatsApp do.
Pass a cancellation token.
Using a third party library is pretty obvious. As for a token, you can make your method take a function in and then call it, as such:
function getWithCancel(url, token) { // the token is for cancellation
var xhr = new XMLHttpRequest;
xhr.open("GET", url);
return new Promise(function(resolve, reject) {
xhr.onload = function() { resolve(xhr.responseText); });
token.cancel = function() { // SPECIFY CANCELLATION
xhr.abort(); // abort request
reject(new Error("Cancelled")); // reject the promise
};
xhr.onerror = reject;
});
};
Which would let you do:
var token = {};
var promise = getWithCancel("/someUrl", token);
// later we want to abort the promise:
token.cancel();
Your actual use case - last
This isn't too hard with the token approach:
function last(fn) {
var lastToken = { cancel: function(){} }; // start with no op
return function() {
lastToken.cancel();
var args = Array.prototype.slice.call(arguments);
args.push(lastToken);
return fn.apply(this, args);
};
}
Which would let you do:
var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc"); // this will get canceled too
synced("/url1?q=abcd").then(function() {
// only this will run
});
And no, libraries like Bacon and Rx don't "shine" here because they're observable libraries, they just have the same advantage user level promise libraries have by not being spec bound. I guess we'll wait to have and see in ES2016 when observables go native. They are nifty for typeahead though.
With AbortController
It is possible to use abort controller to reject promise or resolve on your demand:
let controller = new AbortController();
let task = new Promise((resolve, reject) => {
// some logic ...
const abortListener = ({target}) => {
controller.signal.removeEventListener('abort', abortListener);
reject(target.reason);
}
controller.signal.addEventListener('abort', abortListener);
});
controller.abort('cancelled reason'); // task is now in rejected state
Also it's better to remove event listener on abort to prevent memory leaks
And you can later check if error was thrown by abort by checking the controller.signal.aborted boolean property like:
const res = task.catch((err) => (
controller.signal.aborted
? { value: err }
: { value: 'fallback' }
));
If you would check if task is aborted and just return, then the Promise will be in pending status forever. But in that case you also will not get .catch fired with any error if that's your intension:
controller.abort();
new Promise((resolve, reject) => {
if(controller.signal.aborted) return;
}
Same works for cancelling fetch:
let controller = new AbortController();
fetch(url, {
signal: controller.signal
});
or just pass controller:
let controller = new AbortController();
fetch(url, controller);
And call abort method to cancel one, or infinite number of fetches where you passed this controller
controller.abort();
Standard proposals for cancellable promises have failed.
A promise is not a control surface for the async action fulfilling it; confuses owner with consumer. Instead, create asynchronous functions that can be cancelled through some passed-in token.
Another promise makes a fine token, making cancel easy to implement with Promise.race:
Example: Use Promise.race to cancel the effect of a previous chain:
let cancel = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancel();
let p = new Promise(resolve => cancel = resolve);
Promise.race([p, getSearchResults(term)]).then(results => {
if (results) {
console.log(`results for "${term}"`,results);
}
});
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search: <input id="input">
Here we're "cancelling" previous searches by injecting an undefined result and testing for it, but we could easily imagine rejecting with "CancelledError" instead.
Of course this doesn't actually cancel the network search, but that's a limitation of fetch. If fetch were to take a cancel promise as argument, then it could cancel the network activity.
I've proposed this "Cancel promise pattern" on es-discuss, exactly to suggest that fetch do this.
I have checked out Mozilla JS reference and found this:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Let's check it out:
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
We have here p1, and p2 put in Promise.race(...) as arguments, this is actually creating new resolve promise, which is what you require.
For Node.js and Electron, I'd highly recommend using Promise Extensions for JavaScript (Prex). Its author Ron Buckton is one of the key TypeScript engineers and also is the guy behind the current TC39's ECMAScript Cancellation proposal. The library is well documented and chances are some of Prex will make to the standard.
On a personal note and coming from C# background, I like very much the fact that Prex is modelled upon the existing Cancellation in Managed Threads framework, i.e. based on the approach taken with CancellationTokenSource/CancellationToken .NET APIs. In my experience, those have been very handy to implement robust cancellation logic in managed apps.
I also verified it to work within a browser by bundling Prex using Browserify.
Here is an example of a delay with cancellation (Gist and RunKit, using Prex for its CancellationToken and Deferred):
// by #noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise
const prex = require('prex');
/**
* A cancellable promise.
* #extends Promise
*/
class CancellablePromise extends Promise {
static get [Symbol.species]() {
// tinyurl.com/promise-constructor
return Promise;
}
constructor(executor, token) {
const withCancellation = async () => {
// create a new linked token source
const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
try {
const linkedToken = linkedSource.token;
const deferred = new prex.Deferred();
linkedToken.register(() => deferred.reject(new prex.CancelError()));
executor({
resolve: value => deferred.resolve(value),
reject: error => deferred.reject(error),
token: linkedToken
});
await deferred.promise;
}
finally {
// this will also free all linkedToken registrations,
// so the executor doesn't have to worry about it
linkedSource.close();
}
};
super((resolve, reject) => withCancellation().then(resolve, reject));
}
}
/**
* A cancellable delay.
* #extends Promise
*/
class Delay extends CancellablePromise {
static get [Symbol.species]() { return Promise; }
constructor(delayMs, token) {
super(r => {
const id = setTimeout(r.resolve, delayMs);
r.token.register(() => clearTimeout(id));
}, token);
}
}
// main
async function main() {
const tokenSource = new prex.CancellationTokenSource();
const token = tokenSource.token;
setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms
let delay = 1000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should reach here
delay = 2000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should not reach here
}
main().catch(error => console.error(`Error caught, ${error}`));
Note that cancellation is a race. I.e., a promise may have been resolved successfully, but by the time you observe it (with await or then), the cancellation may have been triggered as well. It's up to you how you handle this race, but it doesn't hurts to call token.throwIfCancellationRequested() an extra time, like I do above.
I faced similar problem recently.
I had a promise based client (not a network one) and i wanted to always give the latest requested data to the user to keep the UI smooth.
After struggling with cancellation idea, Promise.race(...) and Promise.all(..) i just started remembering my last request id and when promise was fulfilled i was only rendering my data when it matched the id of a last request.
Hope it helps someone.
See https://www.npmjs.com/package/promise-abortable
$ npm install promise-abortable
You can make the promise reject before finishing:
// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
let cancel
const promise = new Promise((resolve, reject) => {
cancel = reject
promiseToCancel
.then(resolve)
.catch(reject)
})
return {promise, cancel}
}
// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
timeInMs = time * 1000
setTimeout(()=>{
console.log(`Waited ${time} secs`)
resolve(functionToExecute())
}, timeInMs)
})
// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')
// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))
promise
.then((res) => {
console.log('then', res) // This will executed in 1 second
})
.catch(() => {
console.log('catch') // We will force the promise reject in 0.5 seconds
})
waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve
Unfortunately the fetch call has already be done, so you will see the call resolving in the Network tab. Your code will just ignore it.
Using the Promise subclass provided by the external package, this can be done as follows: Live demo
import CPromise from "c-promise2";
function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {...fetchOptions, signal}).then(resolve, reject)
}, timeout)
}
const chain= fetchWithTimeout('http://localhost/')
.then(response => response.json())
.then(console.log, console.warn);
//chain.cancel(); call this to abort the promise and releated request
Using AbortController
I've been researching about this for a few days and I still feel that rejecting the promise inside an abort event handler is only part of the approach.
The thing is that as you may know, only rejecting a promise, makes the code awaiting for it to resume execution but if there's any code that runs after the rejection or resolution of the promise, or outside of its execution scope, e.g. Inside of an event listener or an async call, it will keep running, wasting cycles and maybe even memory on something that isn't really needed anymore.
Lacking approach
When executing the snippet below, after 2 seconds, the console will contain the output derived from the execution of the promise rejection, and any output derived from the pending work. The promise will be rejected and the work awaiting for it can continue, but the work will not, which in my opinion is the main point of this exercise.
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
console.log( 'Work' );
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
Which leads me to think that after defining the abort event handler there must be calls to
if ( abortController.signal.aborted ) return;
in sensible points of the code that is performing the work so that the work doesn't get performed and can gracefully stop if necessary (Adding more statements before the return in the if block above).
Proposal
This approach reminds me a little about the cancellable token proposal from a few years back but it will in fact prevent work to be performed in vain. The console output should now only be the abort error and nothing more and even, when the work is in progress, and then cancelled in the middle, it can stop, as said before in a sensible step of the processing, like at the beginning of a loop's body
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
if ( abortController.signal.aborted ) return;
console.log( 'Work' );
if ( abortController.signal.aborted ) return;
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
I found the posted solutions here a little hard to read, so I created a helper function that is in my opinion easier to use.
The helper function gives access to to the information whether the current call is already obsolete or not. With this information the function itself has to take care of things accordingly (usually by simply returning).
// Typescript
export function obsoletableFn<Res, Args extends unknown[]>(
fn: (isObsolete: () => boolean, ...args: Args) => Promise<Res>,
): (...args: Args) => Promise<Res> {
let lastCaller = null;
return (...args: Args) => {
const me = Symbol();
lastCaller = me;
const isObsolete = () => lastCaller !== me;
return fn(isObsolete, ...args);
};
}
// helper function
function obsoletableFn(fn) {
let lastCaller = null;
return (...args) => {
const me = Symbol();
lastCaller = me;
const isObsolete = () => lastCaller !== me;
return fn(isObsolete, ...args);
};
}
const simulateRequest = () => new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000));
// usage
const myFireAndForgetFn = obsoletableFn(async(isObsolete, x) => {
console.log(x, 'starting');
await simulateRequest();
if (isObsolete()) {
console.log(x, 'is obsolete');
// return, as there is already a more recent call running
return;
}
console.log(x, 'is not obsolete');
document.querySelector('div').innerHTML = `Response ${x}`;
});
myFireAndForgetFn('A');
myFireAndForgetFn('B');
<div>Waiting for response...</div>
So I have an async function that I needed to cancel on user input, but it's a long running one that involves mouse control.
I used p-queue and added each line in my function into it and have an observable that I feed the cancellation signal. Anything that the queue starts processing will run no matter what but you should be able to cancel anything after that by clearing the queue. The shorter the task you add to the queue, the sooner you can quit after getting the cancel signal. You can be lazy and throw whole chunks of code into the queue instead of the one liners i have in the example.
p-queue releases Version 6 works with commonjs, 7+ switches to ESM and could break your app. Breaks my electron/typescript/webpack one.
const cancellable_function = async () => {
const queue = new PQueue({concurrency:1});
queue.pause();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
for await (const item of items) {
queue.addAll([
async () => await do_something({...}),
async () => await do_something_else({...}),
])
}
const {information} = await get_information();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
cancel_signal$.pipe(take(1)).subscribe(() => {
queue.clear();
});
queue.start();
await queue.onEmpty()
}
Because #jib reject my modify, so I post my answer here. It's just the modfify of #jib's anwser with some comments and using more understandable variable names.
Below I just show examples of two different method: one is resolve() the other is reject()
let cancelCallback = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by resolve()
return resolve('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results == 'Canceled') {
console.log("error(by resolve): ", results);
} else {
console.log(`results for "${term}"`, results);
}
});
}
input2.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by reject()
return reject('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results !== 'Canceled') {
console.log(`results for "${term}"`, results);
}
}).catch(error => {
console.log("error(by reject): ", error);
})
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search(use resolve): <input id="input">
<br> Search2(use reject and catch error): <input id="input2">

Categories