why the timeout operator that I have in my http request does not throw an error - rxjs

the timeout that I defined does not throw any error when the duration parameter I defined is greater than 7000 ms. what is strange is that the timeout operator works well in my code from 0 to 7000 ms
pay(billing: Billing): Observable {
const httpOptions = {
headers: new HttpHeaders({
// 'Access-Control-Allow-Origin':'*'
}),
params: new HttpParams()
.append('timezone', billing.timezone)
.append('mode', billing.mode)
.append('responseFailURL', billing.responseFailURL)
.append('responseSuccessURL', billing.responseSuccessURL)
.append('hash', billing.hash)
.append('txndatetime', billing.txndatetime)
.append('chargetotal', billing.chargetotal.toString())
.append('storename', billing.storename.toString())
.append('currency', billing.currency.toString())
};
// Sending required payment infrmations to Authipay host url
return forkJoin(
of(2), timer(2000).pipe(mergeMap(value => this.getPayementStatus(billing.txndatetime))).pipe( timeout(7500))
).pipe(
map(
([articles, authorOfTheMonth]) => {
console.log(authorOfTheMonth);
return authorOfTheMonth;
}
)
).subscribe(
resp => {
this.router.navigate(['success'], { relativeTo: this.route });
} else {
form.setErrors({ paymentFailed: true });
this.alertify.error(this.translate.instant('error.payment'));
}
},
error => {
if (error instanceof TimeoutError) {
this.alertify.error(error.message);
} else {
this.alertify.error(this.translate.instant('error.payment'));
}
}
);

timeout seems to work as expected to me.
I wrote a test here where I replaced your this.getPayementStatus(billing.txndatetime)) function with a :
simulated response
const simulateResponseTime = (timeInMS)=> timer(timeInMS); // in milliseconds
Which will return a response in delayOfResponse milliseconds. With this tool we can test what happens when the response takes more time than timeout threshold:
Simulation parameters
const timeoutThreshold = 7500; // in ms
const delayOfResponse = 200; //in ms
Finally, a minimalist version of
Your code
forkJoin(of(2), timer(2000).pipe(
mergeMap(value => simulateResponseTime(delayOfResponse))
).pipe(timeout(timeoutThreshold))
).pipe(
...
).subscribe(
resp => {
console.log('Success')
},
error => {
console.log('Error message :', error.message)
console.log('Error type :', error.name)
console.log('Is a TimeoutError :', error.name === 'TimeoutError' )
}
);

Related

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

How to get data from failed forkJoin request?

Using Angular Rxjs and ngrx
I have an action that dispatch 4 API and I am doing the following =>
#Effect()
getAllModels$ = this.actions$.pipe(
ofType<featureActions.GetAllModelsRequest>(featureActions.ActionTypes.GetAllModelsRequest),
switchMap((action) =>
forkJoin([
this.dataService.GetAllModelFromServer(),
this.dataService.GetAllModelFromHost(),
this.dataService.GetAllModelFromCache(),
this.dataService.GetAllModelFromPreference(),
]).pipe(
map(
([server, host, cache, preference]) =>
new featureActions.GetAllModelsSuccess({
//...
})
),
catchError((error: HttpErrorResponse) => {
return of(new featureActions.GetAllModelsFailed({ error: error.message }));
})
)
)
);
The problem is, when one of those API fail, everything fail and I am in fail action. all the data that got retrieved (before the one endpoint that failed) is lost.
Is there a way to get the data retrieved in the catchError or the only solution is to chain the api one after the other ?
You can write your own implementation of forkJoin. Here is a simple example sourced from the original (https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/forkJoin.ts):
export function forkJoin2(...args: any[]): Observable<any> {
const resultSelector = popResultSelector(args);
const { args: sources, keys } = argsArgArrayOrObject(args);
if (resultSelector) {
// deprecated path.
return forkJoinInternal(sources, keys).pipe(map((values: any[]) => resultSelector!(...values)));
}
return forkJoinInternal(sources, keys);
}
function forkJoinInternal(sources: ObservableInput<any>[], keys: string[] | null): Observable<any> {
return new Observable((subscriber) => {
const len = sources.length;
if (len === 0) {
subscriber.complete();
return;
}
const values = new Array(len);
let completed = 0;
let emitted = 0;
for (let sourceIndex = 0; sourceIndex < len; sourceIndex++) {
const source = innerFrom(sources[sourceIndex]);
let hasValue = false;
subscriber.add(
source.subscribe({
next: (value) => {
if (!hasValue) {
hasValue = true;
emitted++;
}
values[sourceIndex] = value;
},
error: (err) => { return subscriber.error({ error: err, values }) },
complete: () => {
completed++;
if (completed === len || !hasValue) {
if (emitted === len) {
subscriber.next(keys ? keys.reduce((result, key, i) => (((result as any)[key] = values[i]), result), {}) : values);
}
subscriber.complete();
}
},
})
);
}
});
}
Notice, when an error occurs, you are returning the error along with the values:
error: (err) => { return subscriber.error({ error: err, values }) }
I went with this solution found here : https://medium.com/better-programming/rxjs-error-handling-with-forkjoin-3d4027df70fc
#Effect()
getAllModels$ = this.actions$.pipe(
ofType<featureActions.GetAllModelsRequest>(featureActions.ActionTypes.GetAllModelsRequest),
switchMap((action) =>
forkJoin([
this.dataService.GetAllModelFromServer().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromHost().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromCache().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromPreference().pipe(catchError(() => of({ data: [] }))),
]).pipe(
map(
([server, host, cache, preference]) =>
new featureActions.GetAllModelsSuccess({
//...
})
),
catchError((error: HttpErrorResponse) => {
return of(new featureActions.GetAllModelsFailed({ error: error.message }));
})
)
)
);

Nest.js handling errors for HttpService

I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).

transform old httpGet to Pipeable operators

I have an abstract.service which does the CRUD operations. Now I am migrating from ionic 3 to ionic 4 but it seems that rxjs is updated and does not allow me to do this anymore:
httpGet(url, requestObject = new RequestObject, onSuccess = (resp, listData) => {}, onFail = (err) => {}) {
let data;
let headers;
return this.http.get(url, {headers: headers, params: data})
.map(response => response).catch(this.handleError).subscribe(
(resp) => {
if (!resp) resp = {items: []};
if (requestObject.loader) this.globalService.onHideLoader.emit(requestObject.loaderId)
onSuccess(resp, {});
requestObject.onDone(resp);
},
(err) => {
if(requestObject.loader) this.globalService.onHideLoader.emit(requestObject.loaderId);
// Execute own errorHandling when response has no errorHandling
// if(!requestObject.hasErrorHandling) this.onError(err);
onFail(err);
requestObject.onDone(err);
}
);
}
So How can I transform this pieve of code above to an example with Pipable operators here is what I have tried:
httpGet(url, requestObject = new RequestObject, onSuccess = (resp, listData) => {}, onFail = (err) => {}) {
let data;
let headers;
return this.http.get(url, {headers: headers, params: data}).pipe(
map(response => {
if (!response) response = {items: []};
if (requestObject.loader) this.globalService.onHideLoader.emit(requestObject.loaderId)
onSuccess(response, {});
requestObject.onDone(response);
}),
catchError(error => {
this.handleError(error);
if(requestObject.loader) this.globalService.onHideLoader.emit(requestObject.loaderId);
// Execute own errorHandling when response has no errorHandling
if(!requestObject.hasErrorHandling) this.onError(err);
onFail(error);
requestObject.onDone(error);
})
);
}
but at the catchError method it throws me this error:
Argument of type '(error: any) => void' is not assignable to parameter of type '(err: any, caught: Observable<void>) => ObservableInput<{}>'.
Can somebody help me out convert the old method to the newer one?

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