rxjs poll for data on timer and reset timerwhen manually refreshed - rxjs

I am using the following libraries in the relevant application: Angular 4.x, ngrx 4.x, rxjs 5.4.x
I have an api that I need to poll every 5 minutes. The user is also able to manually refresh the data. That data is stored in an ngrx store. I am using ngrx effects so the data is retrieved by dispatching an action of type 'FETCH'.
I want to setup a rxjs stream where it will dispatch the 'FETCH' action to the ngrx store. It will be a sliding 5 minute timer that resets when the user manually updates the store. The stream should initially emit a value when subscribed.
I'm not sure how I can reset the timer. In plain javascript I would do something like the following:
console.clear();
let timer;
let counter = 0;
function fetch() {
console.log('fetch', counter++);
poll();
}
function poll() {
if (timer != null) {
window.clearTimeout(timer);
}
timer = window.setTimeout(() => {
console.log('poll');
fetch();
}, 5000);
}
function manualGet() {
console.log('manual');
fetch();
}
fetch();
<button onClick="manualGet()">Get Data</button>
Question: How do I emit on an interval that is reset when another stream emits like the example again?

You want two components to your stream – a timer and some user input. So let's start with the user input. I'll assume some button which can be clicked:
const userInput$ = Observable.fromEvent(button, 'click');
Now we want to start a timer which resets everytime userInput$ emits. We can do that using
userInput$.switchMap(() => Observable.timer(0, 5000));
However, we also want this stream to start without the user having to first click the button. But that's also not a problem:
userInput$.startWith(null);
Now we put it all together:
Observable.fromEvent(button, 'click')
.startWith(null)
.switchMap(() => Observable.timer(0, 5000))
.subscribe(() => dispatchFetch());
Note that I am following your examples of using a 5 second timer, not a 5 minute timer (which you mentioned in the question.)

After writing it out in vanilla JS I realized that the source of the timer should be the data. I was struggling to figure out what the source would be. Clearly it couldn't be the timer since I needed to reset it.
I'm open to better options but here is how I solved it:
console.clear();
let counter = 0;
const data = new Rx.BehaviorSubject(null);
function fetch() {
data.next(counter++);
}
function manualGet() {
console.log('manual');
fetch();
}
// setup poll
data.switchMap(() => Rx.Observable.timer(5000))
.subscribe(() => {
console.log('poll');
fetch();
});
// subscribe to the data
data.filter(x => x != null).
subscribe(x => { console.log('data', x); });
// do the first fetch
fetch();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
<button onClick="manualGet()">Get Data</button>
With ngrx I am listening for the success action related to the fetch event.

Related

Can't understand why async/await isn't working NextJs

I'm looking for a help with how to make a chunk load, when the user scrollbar, and reach a specific div, then it shoul run a function one time, but the code run multiples time:
async function loadMore(){
console.log('i load more');
}
window.addEventListener('scroll', async (event) => {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 1 ) {
document.getElementById('final').style.height = '3000px'
let msg = await loadMore()
console.log('i finish')
document.getElementById('final').style.height = '30px'
}
}, {
passive: true
});
return (
<div id='final'>
<Image src="/ajax-loader.gif" width={60} height={60} alt="loader spinner"></Image>
</div>
)
Few things:
You should add the window event listener in a useEffect.
If you add the event listener in the render phase of the component, it will run add a event listener on every render which might be 1 of the reasons why it is running multiple times.
You also need to cleanup the event listener as a cleanup of useEffect else you will again end up with a lot of event listeners
You need to debounce the onScroll handler
Scroll event is triggered several times when you scroll which is something you might not want. So, adding a debounce will help reduce the number of times, the handler is called when a user scrolls.
Lodash's debounce is 1 popular implementation of debounce. You can choose other implementations if you want or create your own one.
import debounce from "lodash.debounce";
function YourComponent() {
async function loadMore(){
console.log('i load more');
}
// "useEffect" so that you don't add a new event listener
// on every render
useEffect(() => {
const onScroll = async (event) => {
// Whatever you want to do when user scrolls
}
// This is the debounced "onScroll" function instance
// "500" specifies that it will be debounced for 500 milliseconds
const debouncedOnScroll = debounce(onScroll, 500);
// Attach the event listener to window
window.addEventListener('scroll', debouncedOnScroll);
// Cleanup the event listener when component unmounts or
// when the "useEffect" runs again.
return () => window.removeEventListener('scroll', debouncedOnScroll);
}, []);
return (
<div id='final'>
{/* Rest of your JSX */}
</div>
)
}
trigger window event listener inside useEffect
make sure to cleanup the event: return () => window.removeEventListener('scroll', callback);

Jest store state and RTL rendered component's onClick event handler different states

I'm using the following code to test a state-dependent react component using jest and rtl:
test("render author, date and image correctly after going next post", async () => {
const store = configureStore({
reducer: {
data: dataReducer
}
});
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
render(<Post />, { wrapper: Wrapper });
const getSpy = jest.spyOn(axios, 'get').mockReturnValue(mockPostJSON);
await store.dispatch(FETCH_POSTS());
expect(getSpy).toBeCalledWith('https://www.reddit.com/r/EarthPorn/.json');
const beforeClick = await screen.findByTestId('authorAndDate');
expect(beforeClick.innerHTML.toString()).toBe(mockPostsList[0].author + ' - ' + mockPostsList[0].date);
fireEvent.click(screen.getByText('Next post'));
const afterClick = await screen.findByTestId('authorAndDate');
expect(afterClick.innerHTML.toString()).toBe(mockPostsList[1].author + ' - ' + mockPostsList[1].date);
})
The problem I'm having is that before the click everything in the store is set up correctly and the authorAndDate element displays the first item in the array of posts. But after the click is fired the store goes back to the initial state it had before loading the mock data. I checked within the component's event handler and right before it does anything the state has been reset. The code is as follows:
const handleNextClick = () => {
store.dispatch(GO_NEXT_POST());
store.dispatch(FETCH_COMMENTS());
}
I've been an hour over the code trying to find something that would reset the state and found nothing. I'm guessing it's some kind of interaction between jest and rtl but I can't figure out why the store in the test has one state and the store in the component's event handler has another :S
Well, figured it out. Can't use store.dispatch directly as it's accessing a stale state. Needed to use the useDispatch hook. Hope this serves anybody who faces the same problem in the future.

rxjs: cancelling a debounced observable

I have an observable Subject that emits some changes with debouncing:
someSubject.pipe(
debounceTime(5000),
).subscribe(response => {
console.log('Value is', response);
})
Now, I need a Stop button somewhere on the screen that would cancel my debounced emit. So I create a button:
const stopObs = new Subject();
...
<button onClick={() => stopObs.next()}>Stop</button>
and modify my subscription like so:
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
).subscribe(response => {
console.log('Value is', response);
})
This works fine, after hitting "Stop" I stop getting values in console, but there is a problem: the observable is stopped forever. And I need it to be able to emit new values, I only need to cancel already started debounced emits.
My first thought was to create a new subject and use repeatWhen:
const startObs = new Subject();
...
<button onClick={() => startObs.next()}>Start</button>
...
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
repeatWhen(() => startObs)
).subscribe(response => {
console.log('Value is', response);
})
But there's another problem: if I hit "Start" button more than one time and emit more than one value to startObs, then I start getting multiple console.log's for single debounced value!
So is there a way to cancel only debounced emits without stopping the entire observable?
Since debounceTime is just
const duration = timer(dueTime, scheduler);
return debounce(() => duration);
I think you can solve the problem like this:
someSubject.pipe(
debounce(() => timer(5000).pipe(takeUntil(stopObs))),
)
If you want to send the last value when the timer is cancelled due to stopObs, you could try this:
someSubject.pipe(
debounce(
() => timer(5000)
.pipe(
takeUntil(stopObs),
isEmpty(),
)
),
)
isEmpty() will emit true immediately before a complete notification, which is what debounce needs in order to send the last received value. If the timer completes without stopObs's involvement, isEmpty will emit false instead of true, but this still works well for debounce, since it only needs a value from the inner observable.

Ember RSVP promise stopped working

For some reason an RSVP promise I created in an action stopped working for me and I can't figure out what when wrong where to make it stop working.
I have a component that sends an action and waits for a response
// COMPONENT IN ITEM ROUTE
callAjaxAction() {
this.setProperties({working:true});
Ember.RSVP.cast(this.attrs.action()).finally(() => {
Ember.$('.draggable').animate({
left: 0
});
this.setProperties({working:false});
});
}
This particular instance of the component calls this action in the controller
// IN ITEM CONTROLLER
placeBid() {
return new Ember.RSVP.Promise((resolve,reject) => {
if(this.get('winning')) {
this.get('notification')._confirm({message: "You are currently the highest bidder, would you like to continue with your bid?",isConfirm: true}).then(()=>{
console.log('confirmed');
this.send('placeBidActual').then(()=>{
resolve();
}).catch(()=>{
reject();
});
return true;
}).catch(()=>{
resolve();
console.log('denied');
return false;
});
} else {
setTimeout(()=>{
this.send('placeBidActual').then(()=>{
resolve();
}).catch(()=>{
reject();
});
},500);
}
});
}
This action is calling a confirm method on a service and waiting for the user to hit yes in the case of the user already winning this item. Otherwise I'm just calling the actual ajax action right away, that action looks like this
// IN ITEM CONTROLLER
placeBidActual() {
return new Ember.RSVP.Promise((resolve,reject) => {
Ember.$.ajax({
...
}).then((response)=>{
(do some stuff with the response)
resolve();
}, (reason)=>{
(do something with rejection reason)
reject(reason);
});
});
}
In the console I'm getting the error
Uncaught TypeError: Cannot read property 'then' of undefined
On the line where it states this.send('placeBidActual')
UPDATE:
Here is maybe a better explanation to the expected process flow.
The user attempts to place a bid, the user swipes a component over to indicate they wish to bid. The UI at this point shows a loading indicator and waits for the ajax action to complete before it resets the UI.
If the user is not already the highest bidder of the item it will go straight to the ajax action and upon completion signifies to the component to reset the UI.
However, if the user is the highest bidder of the item it should instead show a confirm message (using the notification service I have setup) and wait for the user to either confirm or deny. If they deny it just cancels and signifies to the component to reset the UI. If the user confirms then it calls the ajax action and upon completion signifies to the component to reset the UI.
Updated answer:
This isn't working because send doesn't return the value of the action.
I suggest moving the placeBidActual action to a method on the controller and call it like any normal method. This way you will get the return value and be able to call .then on it.
You should pass a function, whithout invoke it.
Instead this:
Ember.RSVP.cast(this.attrs.action()).finally(() =>
Try it:
Ember.RSVP.cast(this.attrs.action).finally(() =>
Invoking the funcion this.attrs.action() does it pass undefined for the Promise.

Protractor : How to wait for page complete after click a button?

In a test spec, I need to click a button on a web page, and wait for the new page completely loaded.
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click();
// ...Here need to wait for page complete... How?
ptor.waitForAngular();
expect(ptor.getCurrentUrl()).toEqual(url + 'abc#/efg');
Depending on what you want to do, you can try:
browser.waitForAngular();
or
btnLoginEl.click().then(function() {
// do some stuff
});
to solve the promise. It would be better if you can do that in the beforeEach.
NB: I noticed that the expect() waits for the promise inside (i.e. getCurrentUrl) to be solved before comparing.
I just had a look at the source - Protractor is waiting for Angular only in a few cases (like when element.all is invoked, or setting / getting location).
So Protractor won't wait for Angular to stabilise after every command.
Also, it looks like sometimes in my tests I had a race between Angular digest cycle and click event, so sometimes I have to do:
elm.click();
browser.driver.sleep(1000);
browser.waitForAngular();
using sleep to wait for execution to enter AngularJS context (triggered by click event).
You don't need to wait. Protractor automatically waits for angular to be ready and then it executes the next step in the control flow.
With Protractor, you can use the following approach
var EC = protractor.ExpectedConditions;
// Wait for new page url to contain newPageName
browser.wait(EC.urlContains('newPageName'), 10000);
So your code will look something like,
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click();
var EC = protractor.ExpectedConditions;
// Wait for new page url to contain efg
ptor.wait(EC.urlContains('efg'), 10000);
expect(ptor.getCurrentUrl()).toEqual(url + 'abc#/efg');
Note: This may not mean that new page has finished loading and DOM is ready. The subsequent 'expect()' statement will ensure Protractor waits for DOM to be available for test.
Reference: Protractor ExpectedConditions
In this case, you can used:
Page Object:
waitForURLContain(urlExpected: string, timeout: number) {
try {
const condition = browser.ExpectedConditions;
browser.wait(condition.urlContains(urlExpected), timeout);
} catch (e) {
console.error('URL not contain text.', e);
};
}
Page Test:
page.waitForURLContain('abc#/efg', 30000);
I typically just add something to the control flow, i.e.:
it('should navigate to the logfile page when attempting ' +
'to access the user login page, after logging in', function() {
userLoginPage.login(true);
userLoginPage.get();
logfilePage.expectLogfilePage();
});
logfilePage:
function login() {
element(by.buttonText('Login')).click();
// Adding this to the control flow will ensure the resulting page is loaded before moving on
browser.getLocationAbsUrl();
}
Use this I think it's better
*isAngularSite(false);*
browser.get(crmUrl);
login.username.sendKeys(username);
login.password.sendKeys(password);
login.submit.click();
*isAngularSite(true);*
For you to use this setting of isAngularSite should put this in your protractor.conf.js here:
global.isAngularSite = function(flag) {
browser.ignoreSynchronization = !flag;
};
to wait until the click itself is complete (ie to resolve the Promise), use await keyword
it('test case 1', async () => {
await login.submit.click();
})
This will stop the command queue until the click (sendKeys, sleep or any other command) is finished
If you're lucky and you're on angular page that is built well and doesn't have micro and macro tasks pending then Protractor should wait by itself until the page is ready. But sometimes you need to handle waiting yourself, for example when logging in through a page that is not Angular (read how to find out if page has pending tasks and how to work with non angular pages)
In the case you're handling the waiting manually, browser.wait is the way to go. Just pass a function to it that would have a condition which to wait for. For example wait until there is no loading animation on the page
let $animation = $$('.loading');
await browser.wait(
async () => (await animation.count()) === 0, // function; if returns true it stops waiting; can wait for anything in the world if you get creative with it
5000, // timeout
`message on timeout`
);
Make sure to use await
you can do something like this
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click().then(function(){
browser.wait(5000);
});
browser.waitForAngular();
btnLoginEl.click().then(function() { Do Something });
to solve the promise.

Resources