I have a very simple server polling scenario:
call API -> 2. onSuccess -> 3. wait 500ms -> 4. go back to step 1
and
call API -> 2. onError -> 3. finish
I want to be using rxjs as I already use rxjava. But I just can't seem to arrive on a proper solution for my problem.
I've tried timer and interval, but the problem is that they just run infinitely with no handlers to pause them while waiting for server to response or quit altogether when error occurs. Tried using retryWhen, but was not able to get it work at all.
This is what I want:
downloadData() {
console.log('downloading data')
$.getJSON('http://localhost:80')
.done((data) => {
console.log('done' + JSON.stringify(data))
setTimeout(() => { this.downloadData() }, 500)
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log(`Error: ${textStatus}`)
})
}
How to achieve the same thing in rxjs?
You should take a look at the repeat operator:
fromFetch.fromFetch(`https://www.googleapis.com/books/v1/volumes?q=purple cow&maxResults=3`).pipe(
exhaustMap(response => {
if (response.ok) {
// OK return data
return response.json()
} else {
// Server is returning a status requiring the client to try something else.
return of({ error: true, message: `Error ${response.status}` })
}
}),
catchError(err => {
// Network or other error, handle appropriately
return of({ error: true, message: err.message })
}),
filter(resp => !resp.error)
delay(500),
repeat(),
).subscribe()
Related
We are using segment in our application and i need to implement an E2E test in order to verify the number of segment calls, i must be sure that every event will be called only once.
I've been searching for a while, i've found this command that verifies the number of api calls:
Cypress.Commands.add(`verifyCallCount`, (alias, expectedNumberOfCalls) => {
const resolvedAlias = alias[0] === `#` ? alias.substring(1) : alias;
cy.get(`${resolvedAlias}.all`, { timeout: 20000 }).then((calls) => {
cy.wrap(calls.length).should(`equal`, expectedNumberOfCalls);
});
});
I use this command after waiting for the api call:
cy.wait(`#${eventAlias}`, { timeout: 20000 })
.then((interception) => {
return JSON.parse(interception.request.body);
})
.then(() => cy.verifyCallCount(eventAlias, 1););
Here is also the place where i add my alias for the api call.
beforeEach(() => {
cy.intercept('POST', 'https://api.segment.io/v1', (req) => {
const body = JSON.parse(req.body);
if (body.hasOwnProperty('type') && body.type === SampleEvent) {
req.alias = eventAlias;
}
});
});
});
Using this approach, when i run the test on local environment, it passes without any problem. but the same test fails on github's actions. and this is the error:
AssertionError: Timed out retrying after 10000ms: Expected to find element: `eventAlias.all`, but never found it.
I think that the .get() command is not being executed after .wait(), i tried to change the order of the commands, but it's not helping.
How can i fix this problem in github actions?
Is there any other way to verify the number of api calls in cypress?
I appreciate any help, thank you.
The answer you used from here Verify number of times request was made is wrong.
The line const resolvedAlias = alias[0] === '#' ? alias.substring(1) : alias removes the initial #, but it needs to be kept.
Also the timeout in cy.get('${resolvedAlias}.all', { timeout: 20000 }) has no effect, it doesn't wait 20 seconds for all calls to happen.
In your test scenario there may be 0, 1, or 2 calls. You want to fail if there is 0 calls or 2 calls, and pass if there is exactly 1 call.
This is enough to fail if there is 0 calls
cy.wait(`#${eventAlias}`, { timeout: 20000 })
To fail if there are 2 calls, you must use a hard wait, then verify the call count
cy.wait(`#${eventAlias}`, { timeout: 20_000 })
cy.wait(2_000) // wait an interval for any extra call to occur
cy.get(`#${eventAlias}.all`)
.its('length')
.should(`equal`, 1); // if two calls happened in interval, fail here
I notice you mention github actions. I had similar problems when testing an API call in CI, the test runs much slower and cause flakiness.
I suggest mocking the response to get better, more consistent performance from your test.
Ref: Controlling the response
As a bonus, there is no need for any long timeout because your mock replies immediately.
beforeEach(() => {
cy.intercept('POST', 'https://api.segment.io/v1', (req) => {
const body = JSON.parse(req.body);
if (body.hasOwnProperty('type') && body.type === SampleEvent) {
req.alias = eventAlias;
// here send mock response without any network delay
req.reply({
headers: {
Set-Cookie: 'newUserName=Peter Pan;'
},
statusCode: 201,
body: {
name: 'Peter Pan'
}
})
}
});
});
})
it('tests there is only a single POST from app', () => {
cy.wait(`#${eventAlias}`)
cy.wait(100)
cy.get(`#${eventAlias}.all`).then((calls) => {
cy.wrap(calls.length).should(`equal`, 1);
});
})
Your goal is to ensure only 1 API call.
You will need the test to wait and see if a 2nd call occurs.
it('accurately test that only one API call happens', () => {
const numOfRequests = 1
cy.intercept('**/api/*', cy.spy().as('api-spy'))
cy.visit('/');
cy.wait(1000)
cy.get('#api-spy').its('callCount').should('equal', numOfRequests)
})
I tested with a simple page that deliberately calls twice, with a delay 100ms between calls,
<script>
fetch('api/1')
setTimeout(() => fetch('api/2'), 100) // delayed 2nd fetch we want to test for
</script>
Without the hard wait the test gives me a false pass.
I also tried inverting the logic, but it still needs a hard wait to test correctly
cy.intercept('**/api/*', cy.spy().as('api-spy'))
cy.visit('/');
cy.wait(1000)
cy.get('#api-spy').its('callCount')
.should('not.equal', 0)
.and('not.equal', 2) // false pass without hard wait
})
Counting inside the routeHandler that checks body.type
2nd alias for call count
before(() => {
cy.wrap(0).as('eventCount')
})
beforeEach(() => {
cy.intercept('POST', 'https://api.segment.io/v1', (req) => {
const body = JSON.parse(req.body);
if (body.hasOwnProperty('type') && body.type === SampleEvent) {
req.alias = eventAlias;
cy.get('#eventCount').then(count => {
cy.wrap(count + 1).as('eventCount')
})
}
});
});
});
it('checks the count', () => {
cy.visit('/');
cy.wait(1000)
cy.get('#eventCount')
.should('equal', 1)
})
Incrementing a global
let eventCount = 0;
beforeEach(() => {
cy.intercept('POST', 'https://api.segment.io/v1', (req) => {
const body = JSON.parse(req.body);
if (body.hasOwnProperty('type') && body.type === SampleEvent) {
req.alias = eventAlias;
eventCount += 1
}
});
});
});
it('checks the count', () => {
cy.visit('/');
cy.wait(1000)
.then(() => {
cy.wrap(eventCount)
.should('equal', 1)
})
})
When you want to get all of the alias calls, you will need to use # to signify the alias. So the custom command will need to be updated.
Cypress.Commands.add(`verifyCallCount`, (registeredAlias, expectedNumberOfCalls) => {
if(alias[0] !== '#') {
throw new Error ('alias does not start with '#')
}
cy.get(`${registeredAlias}.all`, { timeout: 20000 }).then((calls) => {
cy.wrap(calls.length).should(`equal`, expectedNumberOfCalls);
});
});
Usage
cy.intercept('call').as('call')
// some action to trigger call
cy.wait('#call')
// some other actions
cy.verifyCallCount('#call')
Is there any other way to verify the number of api calls in cypress?
This is a concise way to count the api calls and wait for them to finish.
You can pass a cy.spy() in as a "response" and you can use that to count the number of times the intercept was hit.
Using .should() in the Cypress assertion will wait until the expected number of requests to come back.
it('test', () => {
const numOfRequests = 5;
cy.intercept('https://api.segment.io/v1', cy.spy().as('api-spy'));
// Do something to trigger 5 requests
cy.get('#api-spy').its('callCount').should('equal', numOfRequests);
});
If there are a sequence of different endpoints you are waiting for such as /v1/login followed by a /v1/getData etc, the URL in the cy.intercept may need to use a wildcard.
For example:
cy.intercept('https://api.segment.io/v1/**')
I'm having an issue with a race condition in NgRx. In the example below, I'm asynchronously presenting a loading dialog at about the same time as I'm starting an async remote operation. But the remote operation has the potential to complete and fire dismissLoadingDialog() before the loading dialog is fully built, which results in a console error.
What might be a good strategy in NgRx to complete presentLoadingDialog() before the remote operation begins?
#Effect() fetchServerData$ = this.actions$.pipe(
ofType<FetchServerData>(ActionTypes.FetchServerData),
switchMap(action => {
this.presentLoadingDialog('...loading');
return this.dataService.fetchData(action.payload).pipe(
map(result => {
this.dismissLoadingDialog();
return new FetchServerDataSuccess(result);
}),
catchError(err => of(new FetchServerDataFail(err)))
);
})
);
async presentLoadingDialog(message: string): Promise<void> {
this.isLoading = true;
return this.loadingCtrl
.create({
duration: 5000,
message: message
})
.then(loadingDialog => {
loadingDialog.present().then(() => {
if (!this.isLoading) {
loadingDialog.dismiss();
}
});
});
}
async dismissLoadingDialog() {
this.isLoading = false;
if (!isNullOrUndefined(this.loadingCtrl)): Promise<boolean> {
return this.loadingCtrl.dismiss();
}
}
Ionic's LoadingController create method returns a Promise which resolves when loader creation is complete. You can therefore use it in your effect's Observable chain:
presentLoadingDialog(message: string) {
const loader = this.loadingCtrl
.create({
duration: 5000,
message: message
});
return loader.present();
}
dismissLoadingDialog() {
this.loadingCtrl.dismiss();
}
#Effect() fetchServerData$ = this.actions$.pipe(
ofType<FetchServerData>(ActionTypes.FetchServerData),
switchMap(action => forkJoin(from(this.presentLoadingDialog('...loading'), of(action)),
switchMap(([_, action]) => this.dataService.fetchData(action.payload).pipe(
tap(() => this.dismissLoadingDialog()),
map(result => new FetchServerDataSuccess(result)),
catchError(err => {
this.dismissLoadingDialog();
return of(new FetchServerDataFail(err))
})
))
);
The standard I have seen is you have loading and loaded flags in your state. When you dispatch a load action the reducer updates the state with loading: true and loaded: false before the action fires the http request. The action then switch maps to an action that updates the state with the response and loading: false and loaded: true.
In your component you then have a selector for the loading flag and subscribe to it to open and close the dialog
this.loadingSub = loadings$.subscribe(loading => {
if (loading) {
this.presentLoadingDialog('...loading');
} else {
this.loadingDialog.dismiss();
}
});
unsubscribe in onDestroy
It should be up to your components to show UI components, I think actions calling loading dialogs is not an action concern. Tapping into the heart of state management to call UI components is not a pattern I would recommend.
I have a network call where it's likely that api will throw an 400 error. I want to handle this gracefully.
Right now I do it like below -
private fetchStatus(objectId: string): Observable<string> {
return Observable.create((observer) => {
this.http.get('/api/data-one').subscribe(response => {
if (response.result === 'SUCCESS') {
observer.next('SUCCESS');
} else {
observer.next('DENIED');
}
observer.complete();
},
error => {
observer.next('DENIED');
observer.complete();
});
});
}
But I will prefer doing it with Observable.map operator. The problem with Observable.map is when api throws a 400 the entire observable goes in error mode.
I want to prevent this because this get call is being used in a forkJoin with other calls. Failure of this would mean failure of the entire forkJoin below
forkJoin([
this.http.get('/api/route-2'),
this.http.get('/api/route-1'),
this.fetchStatus('abc')
]).subscribe((responseCollection: any) => {
observer.next({
result1: responseCollection[0],
result2: responseCollection[1],
result3: responseCollection[2]
});
observer.complete();
}, error => observer.error(error));
You can do this with map and catchError.
catchError will catch any error thrown by the source and return a new Observable. This new Observable is what, in your case, will be passed to forkJoin in the case of a HTTP error.
private fetchStatus(objectId: string): Observable<string> {
return this.http.get('/api/data-one').pipe(
map(response => response.result === 'SUCCESS' ? 'SUCCESS' : 'DENIED'),
catchError(error => of('DENIED')),
);
}
I'm running echo server and redis. Private channels work perfectly, and messaging I have built for it works. Now I'm trying to get the whisper to work for the typing status as well but no luck. Does whisper require a pusher to work?
What I have tried on keyup (jquery)
Echo.private(chat- + userid)
.whisper('typing',{e: 'i am is typing...'});
console.log('key up'); // this one works so the keyup is triggered
then I'm of course listening the channel what I am whispering into:
Echo.private(chat- + userid).listenForWhisper('typing', (e) => {
console.log(e + ' this is typing');
});
But I get absolutely nothing anywhere. (debugging on at the echo server, nothing on console etc) Any help how to get this to work would be much appreciated.
Your input event:
$('input').on('keydown', function(){
let channel = Echo.private('chat')
setTimeout( () => {
channel.whisper('typing', {
user: userid,
typing: true
})
}, 300)
})
Your listening event:
Echo.private('chat')
.listenForWhisper('typing', (e) => {
e.typing ? $('.typing').show() : $('.typing').hide()
})
setTimeout( () => {
$('.typing').hide()
}, 1000)
Of course you have to have setup the authentication for this channel ahead of time to ensure that the trusted parties have access:
Broadcast::channel('chat', function ($user) {
return Auth::check();
});
Where $user will be the userid we passed to the user param in our object on the front end.
This is what my ReactJS componentDidMount looks like.
Your listening event.
componentDidMount() {
let timer; // timer variable to be cleared every time the user whispers
Echo.join('chatroom')
.here(...)
.joining(...)
.leaving(...)
.listen(...)
}).listenForWhisper('typing', (e) => {
this.setState({
typing: e.name
});
clearTimeout(timer); // <-- clear
// Take note of the 'clearTimeout' before setting another 'setTimeout' timer.
// This will clear previous timer that will make your typing status
// 'blink' if not cleared.
timer = setTimeout(() => {
this.setState({
typing: null
});
}, 500);
});
}
I've set up an epic that waits for another epic to complete, much like #jayphelps' answer here: Invoking epics from within other epics
However I've found that it only seems to run once. After that I can see the CART_CONFIG_READY action in the console but the DO_THE_NEXT_THING action is not triggered.
I've tried various combinations of mergeMap and switchMap, with and without take but nothing seems to help.
This is (kind of) what my code looks like.
import { NgRedux } from '#angular-redux/store';
import { Observable } from 'rxjs/Observable';
import { ActionsObservable } from 'redux-observable';
export class CartEpicsService {
checkCart = (action$: ActionsObservable<any>, store: NgRedux<any>) => {
return action$.ofType('CHECK_CART')
.switchMap(() => {
console.log('___LISTENING___');
return action$.ofType('CART_CONFIG_READY')
.take(1) // removing this doesn't help
.mergeMap(() => {
console.log('___RECEIVED___');
// do stuff here
return Observable.of({
type: 'DO_THE_NEXT_THING'
});
})
.startWith({
type: 'GET_CART_CONFIG'
});
});
}
getCartConfig = (action$: ActionsObservable<any>, store: NgRedux<any>) => {
return action$.ofType('GET_CART_CONFIG')
.switchMap(() => {
const config = store.getState().config;
// we already have the config
if (config) {
return Observable.of({
type: 'CART_CONFIG_READY'
});
}
// otherwise load it from the server using out HTTP service
return this.http.get('/cart/config')
.switchMap((response) => {
return Observable.concat(
Observable.of({
type: 'CART_CONFIG_SUCCESS'
}),
Observable.of({
type: 'CART_CONFIG_READY'
})
);
})
.catch(error => Observable.of({
type: 'CART_CONFIG_ERROR',
error
}));
});
}
}
For context I need the response from the /cart/config endpoint to check the validity of the cart. I only need to download the config once.
Here is a runnable example on JS Bin:
https://jsbin.com/vovejibuwi/1/edit?js,console
Dang this is definitely a tricky one!
Cause
When state.config === true you return an Observable of CART_CONFIG_READY that emits synchronously, whereas during the first time the http request (or delay, in the jsbin) means it is always going to be async.
Why this makes a difference is in the checkCart epic you return an observable chain that listens for CART_CONFIG_READY with action$.ofType('CART_CONFIG_READY') but also applies a .startWith({ type: 'GET_CART_CONFIG' }). That means that GET_CART_CONFIG is going to be emitted synconously before action$.ofType('CART_CONFIG_READY') is subscribed because startWith is basically shorthand for a concat, which might would make the issue more clear if you're familiar with it. It's nearly exactly the same as doing this:
Observable.concat(
Observable.of({
type: 'GET_CART_CONFIG'
}),
action$.ofType('CART_CONFIG_READY') // not subscribed until prior complete()s
.take(1)
.mergeMap(() => {
// stuff
})
);
So to summarize, what is happening the second time around GET_CART_CONFIG is dispatched synchronously, getCartConfig receives it and sees the config is already in the store so it synchronously dispatches CART_CONFIG_READY. But we are not yet listening for it in checkCart so it goes unanswered. Then that callstack returns and the next Observable in the concat, our action$.ofType('CART_CONFIG_READY') chain, gets subscribed to. But too late, the action it listens for has already been emitted!
Solutions
One way to fix this is to make either the emitting of CART_CONFIG_READY always async, or to start listening for it in the other epic before we dispatch GET_CART_CONFIG.
1. emit CART_CONFIG_READY async
Observable.of accepts a scheduler as its last argument, and RxJS supports several of them.
In this case you could use the AsyncScheduler (macrotask) or the AsapScheduler (microtask). Both will work in this case, but they schedule on different times in the JavaScript event loop. If you're not familiar with event loop tasks, check this out.
I would personally recommend using the AsyncSheduler in this case because it will provide the closest async behavior to making an http request.
import { async } from 'rxjs/scheduler/async';
// later inside your epic...
return Observable.of({
type: 'CART_CONFIG_READY'
}, async);
2. Listen for CART_CONFIG_READY before emitting GET_CART_CONFIG
Because startWith is shorthand for a concat (which we don't want to do) we instead need to use some form of merge, with our ofType chain first so that we listen before emitting.
action$.ofType('CART_CONFIG_READY')
.take(1)
.mergeMap(() => {
// stuff
})
.merge(
Observable.of({ type: 'GET_CART_CONFIG' })
)
// or
Observable.merge(
action$.ofType('CART_CONFIG_READY')
.take(1)
.mergeMap(() => {
// stuff
}),
Observable.of({ type: 'GET_CART_CONFIG' })
)
// both are exactly the same, pick personal preference on appearance
You only need to do one of these solutions, but it wouldn't hurt to do both of them. Offhand I would probably recommend using both just so that things are consistent and expected, even if they are a bit more verbose.
You might also be happy to know that Observable.of accepts any number of items, which will be emitted in order. So you don't need to use concat:
// before
Observable.concat(
Observable.of({
type: 'CART_CONFIG_SUCCESS'
}),
Observable.of({
type: 'CART_CONFIG_READY'
})
)
// after
Observable.of({
type: 'CART_CONFIG_SUCCESS'
}, {
type: 'CART_CONFIG_READY'
})
Thanks so much for the jsbin btw, it made it much easier to debug.
Edit based on your comment:
Out of curiosity did you figure this out through experience or debugging?
A combination of both. I've dealt with a ton of async/scheduled code and ordering is very commonly the source of issues. I scanned the code, mentally picturing execution, noticed the difference in async vs sync depending on codepath, then I made a quick operator to make it easy for me to confirm the order in which any Observable chain is subscribed to.
Observable.prototype.logOnSubscribe = function (msg) {
// defer is a pretty useful Observable to learn if you haven't yet
return Observable.defer(() => {
console.log(msg);
return this; // the original source
});
};
I applied it to several places, but the most important are these two:
action$.ofType('CART_CONFIG_READY')
.take(1)
.mergeMap(() => {
// stuff
})
.logOnSubscribe('listening for CART_CONFIG_READY') // <--- here
.startWith({
type: 'GET_CART_CONFIG'
});
// and in the other epic...
if (hasConfig) {
return Observable.of({
type: 'CART_CONFIG_READY'
})
.logOnSubscribe('emitting CART_CONFIG_READY'); // <--- and here
}
It confirmed that in the second code path CART_CONFIG_READY was getting emitted before the other epic was listening for it.