I want to be able to cancel a started promise from my Vue component, specifically a promise returned by a Vuex action.
My use case is that my Vuex action polls an endpoint for status, and I want to be able to cancel that polling if the user performs a certain action (close function in the example).
I have created a custom CancellablePromise class lifted from another stackoverflow answer, but it isn't working with Vuex.
Cancelable promise class (from https://stackoverflow.com/a/60600274/2152511)
export class CancellablePromise<T> extends Promise<T> {
private onCancel: () => void;
constructor(
executor: (
resolve: (value?: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: (cancelHandler: () => void) => void
) => void
) {
let onCancel: () => void;
super((rs, rj) =>
executor(rs, rj, (ch: () => void) => {
onCancel = ch;
})
);
this.onCancel = onCancel;
}
public cancel(): void {
if (this.onCancel) {
this.onCancel();
}
}
}
Action
async [SomeAction.foo]({ state, dispatch, commit, rootGetters }) {
const cancellablePromise = new CancellablePromise<any>((resolve, reject, onCancel) => {
const interval = setInterval(async () => {
const status = await dispatch(SomeAction.bar);
if (status === "goodstatus") {
clearInterval(interval);
resolve();
} else if (status === "badstatus") {
clearInterval(interval);
reject();
}
}, 2000);
onCancel(() => {
clearInterval(interval);
reject();
});
});
return cancellablePromise;
}
Component
data: (() => {
promise: undefined as CancellablePromise<any> | undefined
}),
async call() {
this.promise = this.$store
.dispatch(SomeAction.foo)
.then(response => {
// do something
}) as CancellablePromise<any>;
},
close(): void {
if (this.promise) {
this.promise.cancel(); // outputs cancel is not a function
}
}
The problem occurs in the close function where this.promise.cancel is not a function.
This seems to me is because the object returned by dispatch is indeed a Promise, not a CancellablePromise. My suspicion comes from looking at the Vuex source which, again, seems to create a new Promise from the Promise returned from the action. I'm not very familiar with Typescript's type system, but unless I'm misreading this code I think my CancellablePromise is "lost" here.
How can I accomplish what I want to do here?
Extentding Promise is messy and unnecessary. It's more normal
to expose a Promise's reject method to the wider world (outside the Promise's constructor), and call it wherever necessary to cause the Promise to adopt its error path.
to race a "cancellation Promise" against the Promise of interest but that's not necessary here as the promisification of the setInterval process makes a reject method available.
Something like this should do it (untested).
Action
async [SomeAction.foo]({ state, dispatch, commit, rootGetters }) {
let reject_, interval;
const promise = new Promise((resolve, reject) => {
reject_ = reject; // externalise the reject method
interval = setInterval(async () => {
const status = await dispatch(SomeAction.bar);
if (status === 'goodstatus') {
resolve();
} else if (status === 'badstatus') {
reject(new Error(status)); // for example
} else {
// ignore other states ???
}
}, 2000);
});
promise.cancel = reject_; // decorate promise with its own reject method.
return promise.always(() => { clearInterval(interval) }); // clear the interval however the promise settles (resolve() or reject() above, or promise.cancel() externally).
}
Component
data: (() => {
cancel: null
}),
async call() {
this.close(new Error('new call was made before previous call completed')); // may be a good idea
let promise = this.$store.dispatch(SomeAction.foo); // don't chain .then() yet otherwise the .cancel property is lost.
this.cancel = promise.cancel; // store the means to force-reject the promise;
return promise.then(response => { // now chain .then()
// do something
})
.catch(error => {
console.log(error);
throw error;
});
},
close(reason): void {
if (this.cancel) {
this.cancel(reason || new Error('cancelled'));
}
}
Related
I'm using axios response interceptor and after I cancel a request, I need to break a promise chain. I don't want to add an error check of canceled request for all requests in my application. I've tried bluebird, but it seems it's just promise cancellation, not chain breaking.
I have to process errors in the first catch. This diagram shows the problem in general. Latest then and catch are in different files.
Promise
.then((response) => {
)
.catch((error) => {
// break promise here
})
.then((response) => {
// skip
// I don't want any extra checks here!
)
.catch((error) => {
// skip
// I don't want any extra checks here!
})
Another option is to throw a custom error that can be caught in a singular catch block at the very end like so:
const errorHandler = require('path/to/handler')
class ProcessingError1 extends Error {
constructor(message) {
super(message);
this.name = "ProcessingError1";
}
}
class ProcessingError2 extends Error {
constructor(message) {
this.message = message;
this.name = "ProcessingError2";
}
}
const process1 = async () => {
throw new ProcessingError1("Somethign went wrong");
};
const process2 = async () => {
return { some: "process" };
};
const someApiCall = async () => ({ some: "response" });
someApiCall()
.then(process1)
.then(process2) // process2 is not run if process1 throws an error
.catch(errorHandler);
// ErrorHandler.js
module.exports = e => {
if (e instanceof ProcessingError1) {
// handle error thrown from process1
}
else if (e instanceof ProcessingError2) {
// handle error thrown from process2
}
else {
// handle other errors as needed..
}
}
just keep only one catch block at the end to collect all the errors triggered by your promise chain.
if one promise throws the following in the chain are not executed
Promise
.then((response) => {
)
.then((response) => {
// skip
// I don't want any extra checks here!
)
.catch((error) => {
// skip
// I don't want any extra checks here!
})
Here is my code:
private loadingData() {
var promise = new Promise<any>((resolve) =>
{
resolve();
});
promise.then(() => {
this.asyncServiceCall();
})
.then(() => this.syncFunctionThree())
};
The asynchronous method asyncServiceCall actually returns a Promise.
private asyncServiceCall(): Promise<any> {
return new Promise((resolve) = > {
resolve();
}).then(() => {
this.functionOne()
})
.then(() => {
this.functionTwo();
});
}
Okay, let's look at functionOne and functionTwo. They are both returning Promise.
private functionOne() {
return new Promise((resolve) => {
this.user['load'] = true;
this.service['user'].get().subscribe(d =>
{
this.user['d'] = d;
},
error => console.error(error),
() => {
this.role['load']=false;
});
resolve();
});
}
private functionTwo() {
return new Promise((resolve) => {
this.service['name'].get().subscribe(d =>
{
this.name['d'] = d;
},
resolve();
});
}
The third method syncFunctionThree will use the data this.user['d'] and this.name['d'] to have some business logic. So I want to call functionOne and functionTwo first then call syncFunctionThree.
By the accepted answer without creating new Promise, I don't get lucky. I found that the
syncFunctionThree was called before the asynchronous methods.
So help me.
You are missing an important part of calling promises inside then() ... you need to return those promises or the then() will resolve immediately and go to the next then() in the chain. That's why functionThree is firing before the asynchronous promise functions resolve
private loadingData() {
var promise = new Promise<any>((resolve) =>
{
resolve();
});
promise.then(() => {
// return the promise this function returns
return this.asyncServiceCall();
// ^^^^
})
// this then() won't fire until asyncServiceCall resolves in previous then()
.then((resultFromPriorThen) => this.syncFunctionThree())
}
Now you don't really need this first promise in loadingData() because you already have a promise to work with returned by asyncServiceCall() and can simplify it to:
private loadingData(): Promise<any> {
return this.asyncServiceCall().then(()=>this.syncFunctionThree());
}
Now to fix asyncServiceCall() the same way:
private asyncServiceCall(): Promise<any> {
return this.functionOne().then(()=>this.functionTwo());
}
Final note: Need to add a catch() in loadingData() in case one of the asynchronous operations has a problem
private functionOne() {
return new Promise((resolve) => {
this.service['user'].get().subscribe(resolve);
});
}
private functionTwo() {
return new Promise((resolve) => {
this.service['name'].get().subscribe(resolve);
});
}
private loadingData() {
Promise.all([functionOne(), functionTwo()]).then(([user, name]) => {
this.user['d'] = user;
this.name['d'] = name;
}).then(() => this.syncFunctionThree())
};
By looking at the method signature if the this.service['user'].get() is an rx observable. you can use this.service['user'].get().toPromise() to get the promise directly. if the this.service['user'].get() has multiple values try this.service['user'].get().pipe(first()) instead
I have a main thunk that gets executed when clicking a button. Inside this thunk I want to call another thunk and wait for it to complete before moving forward. The second thunk executes a promise with nested promises. However, I haven't been able to figure out how to wait for the second thunk to complete its asynchronous operations.
I have tried using the return keyword on my thunk to make the call synchronous. I can't use the async keywords since I need this to work in IE 11.
I have also tried to make my second thunk return a promise and then do something like this dispatch(secondThunk()).then(...) but then it says that my thunk doesn't actually return a promise.
Here is some of my code:
export function mainThunk(): ThunkAction<void, void, void, AnyAction> {
return (dispatch: Dispatch<any>) => {
...do some stuff
dispatch(secondThunk());
...do other stuff
};
}
export function secondThunk(): ThunkAction<void, void, void, AnyAction> {
return (dispatch: Dispatch<any>) => {
return new Promise((resolve: any, reject: any) => {
someAsyncFunction()
.then((response) => {
return Promise.all(someArray.map(someId => {
return someOtherAsyncFunction(someId):
}));
})
.then((responses) => {
response.foreach(response => {
dispatch(someReduxAction(response.someField));
});
})
.then(() => {
resolve();
});
});
};
}
When I run my code the mainThunk is not waiting for the secondThunk to complete before executing. Can you help me figure out how to make this work?
You're almost there. In your mainThunk function you need to wait for the promise to resolve or reject. I've written my demo in Javascript and not Typescript. However the principles are the same. Code-sandbox here
import axios from "axios";
function mainThunk() {
secondThunk()
.then(data => {
console.log("success");
console.log(data);
})
.catch(e => {
console.log("something went wrong");
console.log(e);
});
}
function secondThunk() {
return new Promise((resolve, reject) => {
axios
.get("https://swapi.co/api/people/")
.then(people => {
someOtherAsyncFunction().then(planets => {
const response = {
planets: planets,
people: people
};
resolve(response);
});
})
.catch(e => {
reject(e);
});
});
}
function someOtherAsyncFunction() {
return axios.get("https://swapi.co/api/planets/").then(response => {
return response;
});
}
mainThunk();
I can see in Chrome task manager that the tab in which following code is running eats more and more memory, and it is not released until the promise is resolved
UPDATE
Main idea here is to use a single 'low level' method which would handle "busy" responses from the server. Other methods just pass url path with request data to it and awaiting for a valuable response.
Some anti-patterns was removed.
var counter = 1
// emulates post requests sent with ... axios
async function post (path, data) {
let response = (counter++ < 1000) ? { busy: true } : { balance: 3000 }
return Promise.resolve(response)
}
async function _call (path, data, resolve) {
let response = await post()
if (response.busy) {
setTimeout(() => {
_call(path, data, resolve)
}, 10)
throw new Error('busy')
}
resolve(response.balance)
}
async function makePayment (amount) {
return new Promise((resolve, reject) => {
_call('/payment/create', {amount}, resolve)
})
}
async function getBalance () {
return new Promise((resolve, reject) => {
_call('/balance', null, resolve)
})
}
makePayment(500)
.then(() => {
getBalance()
.then(balance => console.log('balance: ', balance))
.catch(e => console.error('some err: ', e))
})
The first time you call _call() in here:
async function getBalance () {
return new Promise((resolve, reject) => {
_call('/balance', null, resolve)
})
}
It will not call the resolve callback and it will return a rejected promise and thus the new Promise() you have in getBalance() will just do nothing initially. Remember, since _call is marked async, when you throw, that is caught and turned into a rejected promise.
When the timer fires, it will call resolve() and that will resolve the getBalance() promise, but it will not have a value and thus you don't get your balance. By the time you do eventually call resolve(response.balance), you've already called that resolve() function so the promise it belongs to is latched and won't change its value.
As others have said, there are all sorts of things wrong with this code (lots of anti-patterns). Here's a simplified version that works when I run it in node.js or in the snippet here in the answer:
function delay(t, val) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, val), t);
});
}
var counter = 1;
function post() {
console.log(`counter = ${counter}`);
// modified counter value to 100 for demo purposes here
return (counter++ < 100) ? { busy: true } : { balance: 3000 };
}
function getBalance () {
async function _call() {
let response = post();
if (response.busy) {
// delay, then chain next call
await delay(10);
return _call();
} else {
return response.balance;
}
}
// start the whole process
return _call();
}
getBalance()
.then(balance => console.log('balance: ', balance))
.catch(e => console.error('some err: ', e))
It might sound weird, but I'm looking for a way to resolve a promise multiple times. Are there any approaches to make this possible?
Think of the following example:
getPromise() {
const event = new Event('myEvent');
setTimeout(() => {
window.dispatchEvent(event);
}, 5000);
setTimeout(() => {
window.dispatchEvent(event);
}, 7000);
return new Promise((resolve) => {
window.addEventListener('myEvent', () => {
resolve('some value'));
});
resolve('some value'));
});
};
And then .then():
getPromise().then(data => {console.log(data)})
Should give the following result:
some value // initial
some value // after 5000ms
some value // after 7000ms
So I know there are libraries to stream data, but I'm really looking for a native non-callbak approach to achieve this.
How to resolve a promise multiple times?
You can't. Promises can only be resolved once. Once they have been resolved, they never ever change their state again. They are essentially one-way state machines with three possible states pending, fulfilled and rejected. Once they've gone from pending to fulfilled or from pending to rejected, they cannot be changed.
So, you pretty much cannot and should not be using promises for something that you want to occur multiple times. Event listeners or observers are a much better match than promises for something like that. Your promise will only ever notify you about the first event it receives.
I don't know why you're trying to avoid callbacks in this case. Promises use callbacks too in their .then() handlers. You will need a callback somewhere to make your solution work. Can you explain why you don't just use window.addEventListener('myEvent', someCallback) directly since that will do what you want?
You could return a promise-like interface (that does not follow Promise standards) that does call its notification callbacks more than once. To avoid confusion with promises, I would not use .then() as the method name:
function getNotifier() {
const event = new Event('myEvent');
setTimeout(() => {
window.dispatchEvent(event);
}, 500);
setTimeout(() => {
window.dispatchEvent(event);
}, 700);
let callbackList = [];
const notifier = {
notify: function(fn) {
callbackList.push(fn);
}
};
window.addEventListener('myEvent', (data) => {
// call all registered callbacks
for (let cb of callbackList) {
cb(data);
}
});
return notifier;
};
// Usage:
getNotifier().notify(data => {console.log(data.type)})
I have a solution in Typescript.
export class PromiseParty {
private promise: Promise<string>;
private resolver: (value?: string | PromiseLike<string>) => void;
public getPromise(): Promise<string> {
if (!this.promise) {
this.promise = new Promise((newResolver) => { this.resolver = newResolver; });
}
return this.promise;
}
public setPromise(value: string) {
if(this.resolver) {
this.resolver(value);
this.promise = null;
this.resolver = null;
}
}
}
export class UseThePromise {
public constructor(
private promiseParty: PromiseParty
){
this.init();
}
private async init(){
const subscribe = () => {
const result = await this.promiseParty.getPromise();
console.log(result);
subscribe(); //resubscribe!!
}
subscribe(); //To start the subscribe the first time
}
}
export class FeedThePromise {
public constructor(
private promiseParty: PromiseParty
){
setTimeout(() => {
this.promiseParty.setPromise("Hello");
}, 1000);
setTimeout(() => {
this.promiseParty.setPromise("Hello again!");
}, 2000);
setTimeout(() => {
this.promiseParty.setPromise("Hello again and again!");
}, 3000);
}
}