Retryable grpc-web server-streaming rpc - rxjs

I am trying to wrap a grpc-web server-streaming client with rxjs.Observable and be able to perform retries if say the server returns an error.
Consider the following code.
// server
foo = (call: (call: ServerWritableStream<FooRequest, Empty>): void => {
if (!call.request?.getMessage()) {
call.emit("error", { code: StatusCode.FAILED_PRECONDITION, message: "Invalid request" })
}
for (let i = 0; i <= 2; i++) {
call.write(new FooResponse())
}
call.end()
}
// client
test("should not end on retry", (done) => {
new Observable(obs => {
const call = new FooClient("http://localhost:8080").foo(new FooRequest())
call.on("data", data => obs.next(data))
call.on("error", err => {
console.log("server emitted error")
obs.error(err)
})
call.on("end", () => {
console.log("server emitted end")
obs.complete()
})
})
.pipe(retryWhen(<custom retry policy>))
.subscribe(
_resp => () => {},
_error => {
console.log("source observable error")
done()
},
() => {
console.log("source observable completed(?)")
done()
})
})
// output
server emitted error
server emitted end
source observable completed(?)
The server emits the "end" event after(?) emitting "error", so it seems like I have to remove the "end" handler from the source observable.
What would be an "Rx-y" way to end/complete the stream?

For anyone interested, I ended up removing the "end" event handler and replaced it with "status", if the server returns an OK status code (which signals the end of the stream) then the observable is completed.
new Observable(obs => {
const call = new FooClient("http://localhost:8080").foo(new FooRequest())
call.on("data", data => obs.next(data))
call.on("error", err => obs.error(err))
call.on("status", status: grpcWeb.Status => {
if (status.code == grpcWeb.StatusCode.OK) {
return observer.complete()
}
})
})

Related

how to catch error of an asyn function call

I don't succeed catching the error that a async function returns. Using Vuejs 3 with a pinia store, althought I don't think that this is specific to vue or pinia.
In a pinia store I have this function:
const getAccount = async(id, month, year) => {
try {
getData.defaults.headers.common['__authorization__'] = UserStore.jwt
const response = await getData.get(`/use/b/comptes/${id}/${month}/${year}`)
if (response.status === 200) {
// update store state and:
return true
}
return false
} catch (error) {
// => this gets correctly executed when the server responds with 409
// console.error(`erreur catch : ${error.response.data.error}`)
return error.response.data
}
}
I'm calling this function from a component, like so:
watchEffect(() => {
if (route.name === 'comptes') {
getAccount( compte.value.id, route.params.month, route.params.year )
.then(result => { console.log('result', result.error) })
.catch(err => {
// this never gets executed, including when server returns a 409
console.log('err', err)
})
}
})
In other words: in the function call, only the then block gets executed, not the catch.
How do I catch the error of the first function?

API request calls not waiting in Cypress test

I have a request that triggers another request that has a value needed later in the test.
I queued the code that uses the value, but still it is undefined. What an I doing wrong?
let val;
cy.request(api).then(response => {
return fetch(`url-${response.id}`).then(response2 => {
val = response2.id
})
})
cy.then(() => {
console.log('val', val) // undefined
})
Add a Promise around the inner request, and return it.
Cypress automatically waits for promises to resolve.
let val;
cy.request(api).then(response => {
return new Cypress.Promise(resolve => {
fetch(`url-${response.id}`).then(response2 => {
val = response2.id
resolve() // signals to Cypress that 2nd request has completed
})
})
cy.then(() => {
console.log('val', val) // passes
})

Keeping error information and the outer observable alive

To ensure an error doesn't complete the outer observable, a common rxjs effects pattern I've adopted is:
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
// Switch to the result of the inner observable.
switchMap((action) => {
// This service could fail.
return this.service.saySomething(action.payload).pipe(
// Return `null` to keep the outer observable alive!
catchError((error) => {
// What can I do with error here?
return of(null);
})
)
}),
// The result could be null because something could go wrong.
tap((result: Result | null) => {
if (result) {
// Do something with the result!
}
}),
// Update the store state.
map((result: Result | null) => {
if (result) {
return new AppActions.SaySomethingSuccess(result);
}
// It would be nice if I had access the **error** here.
return new AppActions.SaySomethingFail();
}));
});
Notice that I'm using catchError on the inner observable to keep the outer observable alive if the underlying network call fails (service.saySomething(action.payload)):
catchError((error) => {
// What can I do with error here?
return of(null);
})
The subsequent tap and map operators accommodate this in their signatures by allowing null, i.e. (result: Result | null). However, I lose the error information. Ultimately when the final map method returns new AppActions.SaySomethingFail(); I have lost any information about the error.
How can I keep the error information throughout the pipe rather than losing it at the point it's caught?
As suggested in comments you should use Type guard function
Unfortunately I can't run typescript in snippet so I commented types
const { of, throwError, operators: {
switchMap,
tap,
map,
catchError
}
} = rxjs;
const actions = of({payload: 'data'});
const service = {
saySomething: () => throwError(new Error('test'))
}
const AppActions = {
}
AppActions.SaySomethingSuccess = function () {
}
AppActions.SaySomethingFail = function() {
}
/* Type guard */
function isError(value/*: Result | Error*/)/* value is Error*/ {
return value instanceof Error;
}
const observable = actions.pipe(
switchMap((action) => {
return service.saySomething(action.payload).pipe(
catchError((error) => {
return of(error);
})
)
}),
tap((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('tap error')
return;
}
console.log('tap result');
}),
map((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('map error')
return new AppActions.SaySomethingFail();
}
console.log('map result');
return new AppActions.SaySomethingSuccess(result);
}));
observable.subscribe(_ => {
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
I wouldn't try to keep the error information throughout the pipe. Instead you should separate your success pipeline (tap, map) from your error pipeline (catchError) by adding all operators to the observable whose result they should actually work with, i.e. your inner observable.
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
switchMap((action) => this.service.saySomething(action.payload).pipe(
tap((result: Result) => {
// Do something with the result!
}),
// Update the store state.
map((result: Result) => {
return new AppActions.SaySomethingSuccess(result);
}),
catchError((error) => {
// I can access the **error** here.
return of(new AppActions.SaySomethingFail());
})
)),
);
});
This way tap and map will only be executed on success results from this.service.saySomething. Move all your error side effects and error mapping into catchError.

Forkjoin with empty (or not) array of observables

I'm trying to detect when all my observables have completed. I have the following Observables:
let observables:any[] = [];
if(valid){
observables.push(new Observable((observer:any) => {
async(()=>{
observer.next();
observer.complete();
})
}))
}
if(confirmed){
observables.push(new Observable((observer:any) => {
async(()=>{
observer.next();
observer.complete();
})
}))
}
Observable.forkJoin(observables).subscribe(
data => {
console.log('all completed');
},
error => {
console.log(error);
}
);
I need to do something whenever all my functions are completed. Forkjoin seems to work when the observables array is not empty. But when the array is empty, it never gets called. How can I solve this?
you are missing the 3rd callback in subscribe. try this:
Rx.Observable.forkJoin([]).subscribe(
val => {
console.log('next');
},
err => {
console.log('err');
},
() => {
console.log('complete')
}
);
forkJoin on empty array completes immediately.
Updated for RxJS 6:
let rep: Observable<any>[] = [];
for (let i = 0; i < areas.length; i++) { // undetermined array length
rep.push(this.httpService.GET('/areas/' + areas[i].name)); // example observable's being pushed to array
}
if (rep !== []) {
forkJoin(rep).subscribe(({
next: value => {
console.log(value)
}
}));
}
Try this:
import { forkJoin, Observable, of } from 'rxjs';
export function forkJoinSafe<T = any>(array: Observable<T>[]): Observable<T[]> {
if (!array.length) {
return of([])
}
return forkJoin<T>(array);
}
You're missing complete callback. You can pass the third argument or pass an observer object instead of 3 arguments to make event checking more readable.
yourObservable.subscribe({
next: value => console.log(value),
error: error => console.log(error),
complete: () => console.log('complete'),
});

How to resume RxJs Observable Interval on Error

I am merging two Observables.
The first one gets the current temperature on init.
The second one polls at a certain interval the API.
If the Api call fails, then the Observable interval is not resumed.
How could I resume it?
getCurrentTemp(): Observable<number> {
return this.http.get(this.environmentService.getTemperatureUrl())
.map((res: Response) => res.json())
.switchMap(() => res.temp);
}
pollCurrentTemperature(): Subscription {
const temp$ = this.getCurrentTemp();
const tempInterval$ = Observable
.interval(3000)
.flatMap(() => this.getCurrentTemp());
return temp$
.take(1)
.merge(tempInterval$)
.subscribe((temp: number) => {
console.log('temp', temp);
}, (err) => {
console.log('error', err);
// When the api fails my interval does not resume. How can I retry it?
});
}
Any ideas? Ty
Use catch.
Catch: recover from an onError notification by continuing the sequence without error
getCurrentTemp(): Observable<number> {
return this.http.get(this.environmentService.getTemperatureUrl())
.map((res: Response) => res.json())
.catch(error => {
console.log('Error occured');
return Observable.empty();
});
.switchMap(() => res.temp);
}
It will catch the error and silently return an empty observable in its place. In effect, the switchmap will skip over the failed api call silently as it will not emit for the empty observable.
Of course, you could have an alternate behaviour on error, but you need to catch it to avoid the problem you are facing.
Using the http status codes you can retrieve an observable only if its a 200 let's say:
getCurrentTemp(): Observable<number> {
return Observable.from(
[
{ value: 1, status: 200 },
{ value: 2, status: 200 },
{ value: 3, status: 200 },
{ value: 4, status: 200 },
{ value: 5, status: 200 },
{ value: 6, status: 400 }])
.switchMap((x: any) => {
if (x.status === 200) {
return Observable.of(x.value);
}
return Observable.onErrorResumeNext();
});
}
pollCurrentTemperature(): Subscription {
const temp$ = this.getCurrentTemp();
const tempInterval$ = Observable
.interval(3000)
.flatMap(() => this.getCurrentTemp());
return temp$
.take(1)
.merge(tempInterval$)
.subscribe((temp: number) => {
console.log('temp', temp);
}, (err) => {
console.log('error', err);
// When the api fails my interval does not resume. How can I retry it?
});
}
The important bit is this return Observable.onErrorResumeNext();

Resources