RxJS: Run asynchronous code before next or error - javascript

I have the following code:
of(true)
.pipe(
take(1)
)
.subscribe(async (_) => {
await this.close();
await Promise.all(...);
}, _ => {
this.close();
console.error(_);
});
I want to only write once this.close(), however I can't use finalize since it has to be run before Promise.all. Is there something I could use to run a code (and wait for it to resolve) before next or error ?

Is there something I could use to run a code (and wait for it to resolve) before next or error
I think materialize() and dematerialize() can be useful here:
src$.pipe(
// Suppress errors so that `delayWhen()` can run properly
// by converting everything into `Notification` objects
materialize(),
// Wait until `close()` finishes
delayWhen(() => this.close())
// Convert from `Notification` to the initial state: `next` || `error` || `complete`
// so that their handlers can be invoked accordingly
dematerialize(),
)
Note that with this approach it is going to wait for a complete notification as well, which can be avoided with the help of filter().

You could emit a value on error with catchError. This way your next callback will be executed when an error occurs.
const ERROR_TOKEN = 'my-error';
of(true).pipe(
take(1),
catchError(_ => {
console.error(_);
return of(ERROR_TOKEN); // emit some kind of error value
})
)
.subscribe(async (value) => {
// do on normal and error value
await this.close();
if (value !== ERROR_TOKEN) {
// only do on normal values
await Promise.all(...);
}
});
Or write a custom operator that merges your async code into the stream.
function awaitOnNextAndError<T, R extends ObservableInput<any>>(
asyncOnNextAndError: () => R,
sideEffectOnError?: (e) => void
): MonoTypeOperatorFunction<T> {
// only run your async code on 'subscribe' and ignore its output
const pre = defer(() => asyncOnNextAndError()).pipe(ignoreElements())
return (source: Observable<T>) => source.pipe(
// do some side effect on errors immediately, e.g. logging
tap({ error: sideEffectOnError }),
// execute your async code before you continue with the value from the stream
mergeMap(v => concat(pre, of(v))),
// execute your async code on error
catchError(e => pre)
)
}
of(true).pipe(
take(1),
awaitOnNextAndError(() => this.close(), console.error)
)
.subscribe(async (_) => {
await Promise.all(...);
});
https://stackblitz.com/edit/rxjs-efscr8

Related

NGRX - dispatch action on catchError effect with different payload

I need to call an API that can return errors, warnings or success.
If it returns warnings the user must able to accept the warning and I should send the same payload + acceptWarning: true.
I need to display an ionic modal and wait for the user's response to see if he accepts or cancel the warning.
What should be the best way to achieve that?
Right now I have something like this:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
exhaustMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast(' Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
switchMap(({ data = '' }) => {
if (data === 'Accept') {
return of(new Assign({ ...assignment, acceptWarning: true }));
}
return of(new AssignShipmentFailure());
})
)
)
)
)
);
async openErrorModal(response: any) {
const errorModal = await this.modalCtrl.create({
component: ErrorValidationPopup,
componentProps: {
response: response,
},
});
await errorModal.present();
return errorModal.onDidDismiss();
}
But it is not triggering the Assign action again. Thanks for your help
If any error occurred in the effect's observable (or any Observable), then its stream emitted no value and it immediately errored out. After the error, no completion occurred, and the Effect will stop working.
To keep the Effect working if any error occurred, you have to swichMap instead of exhaustMap, and handle the errors within the inner observable of the switchMap, so the main Observable won't be affected by that.
Why use switchMap?
The main difference between switchMap and other flattening operators is the cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase switch to a new observable
You can try something like the following:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
switchMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast('Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
map(({ data = '' }) => {
if (data === 'Accept') {
return new Assign({ ...assignment, acceptWarning: true });
}
return new AssignShipmentFailure();
})
)
)
)
)
);

How do I make a new call to an api when a previous one finished successfully?

I am new to angular and rxjs, and I have the following scenario, in which I need that after a call to an api is successfully resolved to make a new call, in the context of angular / rxjs I don't know how to do it
handler(): void {
this.serviceNAme
.createDirectory(this.path)
.pipe(
finalize(() => {
this.someProperty = false;
})
)
.subscribe(
(data) => console.log(data),
(error) => console.error(error.message)
);
}
What is the correct way to make a new call to an api when a previous one was successful?
I understand you have a serviceOne and a serviceTwo. And you want to call serviceTwo using the retrieved data from serviceOne.
Using rxjs switchMap you can pipe one observable into another.
handler(): void {
this.serviceOne
.createDirectory(this.path)
.pipe(
switchMap(serviceOneResult => {
// transform data as you wish
return this.serviceTwo.methodCall(serviceOneResult);
})
)
.subscribe({
next: serviceTwoResult => {
// here we have the data returned by serviceTwo
},
error: err => {},
});
}
If you don't need to pass the data from serviceOne to serviceTwo but you need them to be both completed together, you could use rxjs forkJoin.
handler(): void {
forkJoin([
this.serviceOne.createDirectory(this.path),
this.serviceTwo.methodCall()
])
.subscribe({
next: ([serviceOneResult, serviceTwoResult]) => {
// here we have data returned by both services
},
error: err => {},
});
}
Using aysnc and await you can do:
async handler(): void {
await this.serviceNAme
.createDirectory(this.path)
.pipe(
finalize(() => {
this.someProperty = false;
})
)
.subscribe(
(data) => console.log(data),
(error) => console.error(error.message)
);
// Do second api call
}
There are a few says to do this:
Scenario # 1
Your two service api calls are independent, you just want one to go and then the next
const serviceCall1 = this.serviceName.createDirectory(this.path);
const serviceCall2 = this.serviceName.createDirectory(this.otherPath);
concat(serviceCall1 , serviceCall2).subscribe({
next: console.log,
error: err => console.error(err.message),
complete: () => console.log("service call 1&2 complete")
});
Scenario # 2
Your two calls dependant on one another, so you need the result of the first before you can start the second
this.serviceName.getDirectoryRoot().pipe(
switchMap(root => this.serviceName.createDirectoryInRoot(root, this.path))
).subscribe({
next: console.log,
error: err => console.error(err.message),
complete: () => console.log("service call 1 used to create service call 2, which is complete")
});
You'll want scenario # 2, because done this way, an error in the first call will mean no result is sent to the switchMap, and second call is never made.

Cannot get result of first HTTP request using RxJS' switchMap

I am trying to make 2 HTTP requests and in the first call I try to create a record and then according to its results (response from the API method) I want to execute or omit the second call that updates another data. However, although I can catch the error in catchError block, I cannot get the response in the switchMap method of the first call. So, what is wrong with this implementation according to teh given scenario? And how can I get the response of the first result and continue or not to the second call according to this first response?
let result;
let statusCode;
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
// I am trying to get the response of first request at here
statusCode = response.statusCode;
if(...){
return this.demoService.update(...).pipe(
catchError((err: any) => { ... }),
map(response => {
return {
result: response
}
}
)
)}
}
))
.subscribe(result => console.log(result));
The question is still vague to me. I'll post a more generic answer to make few things clear
There are multiple things to note
When an observable emits an error notification, the observable is considered closed (unless triggered again) and none of the following operators that depend on next notifications will be triggered. If you wish to catch the error notifications inside the switchMap, you could return a next notification from the catchError. Something like catchError(error => of(error)) using RxJS of function. The notification would then be caught by the following switchMap.
You must return an observable from switchMap regardless of your condition. In this case if you do not wish to return anything when the condition fails, you could return RxJS NEVER. If you however wish to emit a message that could be caught by the subscriptions next callback, you could use RxJS of function. Replace return NEVER with return of('Some message that will be emitted to subscription's next callback');
import { of, NEVER } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
statusCode = response.statusCode;
if (someCondition) {
return this.demoService.update(...).pipe( // emit `update()` when `someCondition` passes
catchError((err: any) => { ... }),
map(response => ({ result: response }))
);
}
// Show error message
return NEVER; // never emit when `someCondition` fails
}
)).subscribe({
next: result => console.log(result),
error: error => console.log(error)
});
You can implement with iif
this.demoService
.create(...)
.pipe(
// tap first to be sure there's actually a response to process through
tap(console.log),
// You can use any condition in your iif, "response.user.exists" is just a sample
// If the condition is true, it will run the run the update$ observable
// If not, it will run the default$
// NOTE: All of them must be an observable since you are inside the switchMap
switchMap(response =>
iif(() =>
response.user.exists,
this.demoService.update(response.id), // Pass ID
of('Default Random Message')
)
),
catchError((err: any) => { ... })
);

jest jumps out of function with promise

I'm trying to test a function which calls another module's function which returns a promise,
The problem is that jest does not wait for completion of the myFunction and jumps out of it and treat it as a promise, as result section shows the "done" message is printed before "resolve" message. I have work around using setImmediate but I rather not to use it and want to understand the reason.
the simplified version of the code is following:
The module which is mocked
// repo.js
const getItems = () => {
console.log('real');
return new Promise((resolve, reject) => {
setTimeout(
() => resolve('result'), 1000);
}
);
}
module.exports = {
getItems
};
Unit under test:
// sample.js
const repo = require('./repo');
const myFunction = (req, res) => {
console.log('myFunction');
repo.getItems()
.then(goals => {
console.log('resolve');
res.val = 'OK';
}).catch(err => {
console.log('reject');
res.val = 'Failed';
});
return;
};
module.exports = {myFunction};
Test file:
// sample.test.js
const repo = require('./repo');
const sample = require('./sample');
const result = {
'message': 'done'
};
describe('Tests for receiving items', () => {
it('should call and be successful. ', () => {
repo.getItems = jest.fn(() => {
console.log('mocking');
return new Promise((resolve) => ( resolve(result) ));
});
const response = {val: 'test'};
const request = {};
sample.myFunction(request, response);
console.log('done');
expect(response.val).toBe('OK');
})
}
);
The result is:
console.log MySample\sample.js:5
myFunction
console.log MySample\sampel.test.js:11
mocking
console.log MySample\sampel.test.js:17
done
console.log MySample\sample.js:9
resolve
Error: expect(received).toBe(expected)
Expected value to be (using ===):
"OK"
Received:
"test"
Expected :OK
Actual :test
The test you wrote reflects the correct usage, and you might say it fulfilled its purpose, because it uncovered a bug in your implementation.
To show what exactly went wrong, I will get rid of everything that is not needed, which leads to an even more minimal example. The following test file can be run by Jest and it reproduces your problem.
const myFunction = (res) => {
Promise.resolve()
.then(goals => {
res.val = 'OK';
}).catch(err => {
res.val = 'Failed';
});
return;
};
it('should call and be successful. ', () => {
const response = {val: 'test'};
myFunction(response);
expect(response.val).toBe('OK');
})
myFunction starts a promise (which resolves immediately here with no value) and returns nothing (undefined). You can also test the error part by using Promise.reject instead of Promise.resolve. When you call myFunction(response) the next line is executed when myFunction finishes. This is not when the promise actually finishes, but the function itself. The promise could take any amount of time and there is no way for you tell when it actually finished.
To be able to know when the promise finished, you need to return it, so you can use a .then() on it to execute something after the promise has been resolved. Both .then() and .catch() return a new promise which resolves with the returned value, which in this case is again undefined. That means you need to do your assertion in the .then() callback. Similarly, Jest thinks that the test ends as you exit the function even though it should wait for the promise to be settled. To achieve this you can return the promise from the test and Jest will wait for its completion.
const myFunction = (res) => {
// Return the promise from the function, so whoever calls myFunction can
// wait for the promise to finish.
return Promise.resolve()
.then(goals => {
res.val = 'OK';
}).catch(err => {
res.val = 'Failed';
});
};
it('should call and be successful. ', () => {
const response = {val: 'test'};
// Return the promise, so Jest waits for its completion.
return myFunction(response).then(() => {
expect(response.val).toBe('OK');
});
})
You can also use async/await, but keep in mind that you still need to understand how promises work, as it uses promises underneath. An async function always returns a promise, so Jest knows to wait for its completion.
it('async/await version', async () => {
const response = {val: 'test'};
// Wait for the promise to finish
await myFunction(response);
expect(response.val).toBe('OK');
})
Usually you would also return a value from the promise (in .then() or .catch()) instead of mutating an outer variable (res). Because if you use the same res for multiple promises, you will have a data race and the outcome depends on which promises finished first, unless you run them in sequence.

Observable Finally on Subscribe

According to this artcle, onComplete and onError function of the subscribe are mutually exclusive.
Meaning either onError or onComplete events will fire up in my subscribe.
I have a logic block which needs to be executed whether I receive an error, or I finish my steam of information successfully.
I looked up for something like finally in python, but all I found is finally which needs to be attached to the observable I create.
But I want to to do that logic only when I subscribe, and after the stream has ended, whether successfully or with an error.
Any ideas?
The current "pipable" variant of this operator is called finalize() (since RxJS 6). The older and now deprecated "patch" operator was called finally() (until RxJS 5.5).
I think finalize() operator is actually correct. You say:
do that logic only when I subscribe, and after the stream has ended
which is not a problem I think. You can have a single source and use finalize() before subscribing to it if you want. This way you're not required to always use finalize():
let source = new Observable(observer => {
observer.next(1);
observer.error('error message');
observer.next(3);
observer.complete();
}).pipe(
publish(),
);
source.pipe(
finalize(() => console.log('Finally callback')),
).subscribe(
value => console.log('#1 Next:', value),
error => console.log('#1 Error:', error),
() => console.log('#1 Complete')
);
source.subscribe(
value => console.log('#2 Next:', value),
error => console.log('#2 Error:', error),
() => console.log('#2 Complete')
);
source.connect();
This prints to console:
#1 Next: 1
#2 Next: 1
#1 Error: error message
Finally callback
#2 Error: error message
Jan 2019: Updated for RxJS 6
The only thing which worked for me is this
fetchData()
.subscribe(
(data) => {
//Called when success
},
(error) => {
//Called when error
}
).add(() => {
//Called when operation is complete (both success and error)
});
I'm now using RxJS 5.5.7 in an Angular application and using finalize operator has a weird behavior for my use case since is fired before success or error callbacks.
Simple example:
// Simulate an AJAX callback...
of(null)
.pipe(
delay(2000),
finalize(() => {
// Do some work after complete...
console.log('Finalize method executed before "Data available" (or error thrown)');
})
)
.subscribe(
response => {
console.log('Data available.');
},
err => {
console.error(err);
}
);
I have had to use the add medhod in the subscription to accomplish what I want. Basically a finally callback after the success or error callbacks are done. Like a try..catch..finally block or Promise.finally method.
Simple example:
// Simulate an AJAX callback...
of(null)
.pipe(
delay(2000)
)
.subscribe(
response => {
console.log('Data available.');
},
err => {
console.error(err);
}
)
.add(() => {
// Do some work after complete...
console.log('At this point the success or error callbacks has been completed.');
});

Categories