RxJS: show loading if request is slow - javascript

I thought of using RxJS to solve elegantly this problem, but after trying various approaches, I couldn't find out how to do it...
My need is quite common: I do a Rest call, ie. I have a Promise.
If the response comes quickly, I just want to use the result.
If it is slow to come, I want to display a spinner, until the request completes.
This is to avoid a flash of a the spinner, then the data.
Maybe it can be done by making two observables: one with the promise, the other with a timeout and showing the spinner as side effect.
I tried switch() without much success, perhaps because the other observable doesn't produce a value.
Has somebody implemented something like that?

Based on #PhiLho's answer, I wrote a pipeable operator, which does exactly that:
export function executeDelayed<T>(
fn : () => void,
delay : number,
thisArg? : any
) : OperatorFunction<T, T> {
return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
let timerSub = timer(delay).subscribe(() => fn());
return source.pipe(
tap(
() => {
timerSub.unsubscribe();
timerSub = timer(delay).subscribe(() => fn());
},
undefined,
() => {
timerSub.unsubscribe();
}
)
);
}
}
Basically it returns a function, which gets the Observable source.
Then it starts a timer, using the given delay.
If this timer emits a next-event, the function is called.
However, if the source emits a next, the timer is cancelled and a new one is startet.
In the complete of the source, the timer is finally cancelled.
This operator can then be used like this:
this.loadResults().pipe(
executeDelayed(
() => this.startLoading(),
500
)
).subscribe(results => this.showResult())
I did not wirte many operators myself, so this operator-implementation might not be the best, but it works.
Any suggestions on how to optimize it are welcome :)
EDIT:
As #DauleDK mentioned, a error won't stop the timer in this case and the fn will be called after delay. If thats not what you want, you need to add an onError-callback in the tap, which calls timerSub.unsubscribe():
export function executeDelayed<T>(
fn : () => void,
delay : number,
thisArg? : any
) : OperatorFunction<T, T> {
return function executeDelayedOperation(source : Observable<T>) : Observable<T> {
let timerSub = timer(delay).subscribe(() => fn());
return source.pipe(
tap(
() => {
timerSub.unsubscribe();
timerSub = timer(delay).subscribe(() => fn());
},
() => timerSub.unsubscribe(), // unsubscribe on error
() => timerSub.unsubscribe()
)
);
}
}

Here is an example that I have used. We assume here that you get the data that you want to send to the server as an Observable as well, called query$. A query coming in will then trigger the loadResults function, which should return a promise and puts the result in the results$ observable.
Now the trick is to use observable$.map(() => new Date()) to get the timestamp of the last emitted value.
Then we can compare the timestamps of the last query and the last response that came in from the server.
Since you also wanted to not only show a loading animation, but wanted to wait for 750ms before showing the animation, we introduce the delayed timestamp. See the comments below for a bit more explanation.
At the end we have the isLoading$ Observable that contains true or false. Subscribe to it, to get notified when to show/hide the loading animation.
const query$ = ... // From user input.
const WAIT_BEFORE_SHOW_LOADING = 750;
const results$ = query$.flatMapLatest(loadResults);
const queryTimestamp$ = query$.map(() => new Date());
const resultsTimestamp$ = results$.map(() => new Date());
const queryDelayTimestamp$ = (
// For every query coming in, we wait 750ms, then create a timestamp.
query$
.delay(WAIT_BEFORE_SHOW_LOADING)
.map(() => new Date())
);
const isLoading$ = (
queryTimestamp$.combineLatest(
resultsTimestamp$,
queryDelayTimestamp$,
(queryTimestamp, resultsTimestamp, delayTimestamp) => {
return (
// If the latest query is more recent than the latest
// results we got we can assume that
// it's still loading.
queryTimestamp > resultsTimestamp &&
// But only show the isLoading animation when delay has passed
// as well.
delayTimestamp > resultsTimestamp
);
}
)
.startWith(false)
.distinctUntilChanged()
);

OK, thinking more about it in my commuting, I found a solution...
You can find my experiment ground at http://plnkr.co/edit/Z3nQ8q
In short, the solution is to actually subscribe to the observable handing the spinner (instead of trying to compose it in some way).
If the result of the Rest request comes before the observable fires, we just cancel the spinner's disposable (subscription), so it does nothing.
Otherwise, the observable fires and display its spinner. We can then just hide it after receiving the response.
Code:
function test(loadTime)
{
var prom = promiseInTime(loadTime, { id: 'First'}); // Return data after a while
var restO = Rx.Observable.fromPromise(prom);
var load = Rx.Observable.timer(750);
var loadD = load.subscribe(
undefined,
undefined,
function onComplete() { show('Showing a loading spinner'); });
restO.subscribe(
function onNext(v) { show('Next - ' + JSON.stringify(v)); },
function onError(e) { show('Error - ' + JSON.stringify(e)); loadD.dispose(); },
function onComplete() { show('Done'); loadD.dispose(); }
);
}
test(500);
test(1500);
Not sure if that's an idiomatic way of doing this with RxJS, but it seems to work...
Other solutions are welcome, of course.

Just before fetching the data, ie. creating the spinner, set timeout for a function, which creates the spinner. Lets say you are willing to wait half a second, until showing spinner... it would be something like:
spinnerTimeout = setTimeout(showSpinner, 500)
fetch(url).then(data => {
if (spinner) {
clearTimeout(spinnerTimeout) //this is critical
removeSpinnerElement()
}
doSomethingWith(data)
});
EDIT: if it's not obvious, clearTimer stops the showSpinner from executing, if the data arrived sooner than 500ms(ish).

Here is my solution :
public static addDelayedFunction<T>(delayedFunction: Function, delay_ms: number): (mainObs: Observable<T>) => Observable<T> {
const stopTimer$: Subject<void> = new Subject<void>();
const stopTimer = (): void => {
stopTimer$.next();
stopTimer$.complete();
};
const catchErrorAndStopTimer = (obs: Observable<T>): Observable<T> => {
return obs.pipe(catchError(err => {
stopTimer();
throw err;
}));
};
const timerObs: Observable<any> = of({})
.pipe(delay(delay_ms))
.pipe(takeUntil(stopTimer$))
.pipe(tap(() => delayedFunction()));
return (mainObs: Observable<T>) => catchErrorAndStopTimer(
of({})
.pipe(tap(() => timerObs.subscribe()))
.pipe(mergeMap(() => catchErrorAndStopTimer(mainObs.pipe(tap(stopTimer)))))
);
}

Related

RxJS: How to delay next emitted value?

I have async function handleParamsChanges which can take a few seconds to resolve. And I'm calling it when observable emits value:
this._activatedRoute.params
.subscribe(params => {
this.handleParamsChanges(params).then(() => {
// new value can be processed now
});
});
How could I modify my code so that if observable emits 2 values one after another, first handleParamsChanges is called for first value, and only after this promise resolves, it's called with second value, and so on.
Edit:
Here is the solution I came up with, but I'm guessing there is a better way to do this:
const params$ = this._activatedRoute.params;
const canExecute$ = new BehaviorSubject(true);
combineLatest(params$, canExecute$)
.pipe(
filter(([_, canExecute]) => canExecute),
map(([params]) => params),
distinctUntilChanged()
)
.subscribe(async params => {
canExecute$.next(false);
try {
await this.handleParamsChanges(params);
} catch (e) {
console.log(e);
} finally {
canExecute$.next(true);
}
})
I'm using canExecute$ to delay processing of new value.
I need to use distinctUntilChanged here to avoid creating infinite loop.
What you're looking for is concatMap. It waits for the previous "inner observable" to complete before subscribing again. Also you can greatly simplify your pipe:
params$.pipe(
concatMap(params => this.handleParamsChanges(params)),
).subscribe()

TakeUntil with Checkbox Blocks Stream Even When Evaluation is True

I'm trying to use takeUntil to stop a stream once a checkbox is switched off. I'm in Angular so for now I'm just concatenating a sting on a class property, not worrying about concatenating results of observables yet.
this.checked$ is a BehaviorSubject that starts false and gets nexted when my checkbox is de/activated. That part is for sure working at least somewhat because below will start to display the dots. However, adding the (commented out below) takeUntil(this.checked$) results in no dots displaying at all.
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
filter(c => {
return c === true;
}),
switchMap(() => {
console.log("switchMap");
// return source$.pipe(takeUntil(this.checked$));
return source$;
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
What is incorrect about how I am using BehaviorSubject and takeUntil here?
Here is a full example is on StackBlitz.
Note that takeUntil emits values only until the first checked$ is emited, here are good examples. Because checked$ is a BehaviorSubject it emits the current value immediately when you subscribe to it, as a result takeUntil stops.
A solution might be to switch between source$ and an empty observable inside the switchMap, like:
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
switchMap(c => {
console.log("switchMap");
return c ? source$ : EMPTY;
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
StackBlitz example
You can also fix your original solution by skipping the first value of checked$ subject, but I recommend you the previous solution because it's simpler.
const source$ = interval(250).pipe(mapTo("."));
this.checked$
.pipe(
filter(c => {
return c === true;
}),
switchMap(() => {
console.log("switchMap");
// return source$.pipe(takeUntil(this.checked$));
return source$.pipe(takeUntil(this.checked$.pipe(skip(1))));
})
)
.subscribe(data => {
console.log("in subscribe", data, this.dots);
this.dots += data;
});
StackBlitz example
Another solution is to use Subject instead of BehaviorSubject. The difference is that the Subject doesn't hold current value and emits nothing when you subscribe to it. It emits only when next() is called. If you replace BehaviorSubject with Subject in your original solution, it will work as expected.
StackBlitz example

RxJs: How to only maintain the latest value until inner observable complete

I'm new to RxJs and having trouble to achieve this in "RxJs way":
An infinite stream a$ emits a value a once a while.
async() takes the a and performs an async operation.
If a$ emits values while async is pending, only keep the latest one al.
After the previous async completes, if there is an al, run async(al).
And so on.
a$:----a1----------a2----a3-----------------------a4-----------
async(a1):------------end async(a4):---
async(a3):-----end
Here is what I came up with, a bit nasty:
var asyncIdle$ = new Rx.BehaviorSubject()
var asyncRunning$ = new Rx.Subject()
var async$ = asyncIdle$
function async (val) {
async$ = asyncRunning$
// do something with val
console.log(val + ' handling')
setTimeout(() => {
console.log(val + ' complete')
async$.next()
async$ = asyncIdle$
}, 2000)
}
// simulate a$
var a$ = Rx.Observable.fromEvent(document, 'click')
.mapTo(1)
.scan((acc, curr) => acc + curr)
.do(val => console.log('got ' + val))
a$.debounce(() => async$)
.subscribe(val => {
async(val)
})
I came up with this solution in typescript:
I have a simple Gate class that can be open or closed:
enum GateStatus {
open = "open",
closed = "closed"
}
class Gate {
private readonly gate$: BehaviorSubject<GateStatus>;
readonly open$: Observable<GateStatus>;
readonly closed$: Observable<GateStatus>;
constructor(initialState = GateStatus.open) {
this.gate$ = new BehaviorSubject<GateStatus>(initialState);
this.open$ = this.gate$
.asObservable()
.pipe(filter(status => status === GateStatus.open));
this.closed$ = this.gate$
.asObservable()
.pipe(filter(status => status === GateStatus.closed));
}
open() {
this.gate$.next(GateStatus.open);
}
close() {
this.gate$.next(GateStatus.closed);
}
}
The operator function is quite simple. At the beginning the gate is open. Before we start a request, we close it and when the request has finished we open it again.
The audit() will only let the most recent request-data pass when the gate is open.
export const requestThrottle = <T>(
requestHandlerFactory: (requestData: T) => Observable<any>
) => (requestData: Observable<T>) => {
const gate = new Gate();
return requestData.pipe(
audit(_ => gate.open$),
// NOTE: when the order is important, use concatMap() instead of mergeMap()
mergeMap(value => {
gate.close();
return requestHandlerFactory(value).pipe(finalize(() => gate.open()));
})
);
};
use it like this:
src.pipe(
requestThrottle(() => of(1).pipe(delay(100)))
);
code exmaple on stackblitz
You can use the audit operator to solve the problem, like this (the comments should explain how it works):
// Simulate the source.
const source = Rx.Observable.merge(
Rx.Observable.of(1).delay(0),
Rx.Observable.of(2).delay(10),
Rx.Observable.of(3).delay(20),
Rx.Observable.of(4).delay(150),
Rx.Observable.of(5).delay(300)
).do(value => console.log("source", value));
// Simulate the async task.
function asyncTask(value) {
return Rx.Observable
.of(value)
.do(value => console.log(" before async", value))
.delay(100)
.do(value => console.log(" after async", value));
}
// Compose an observable that's based on the source.
// Use audit to ensure a value is not emitted until
// the async task has been performed.
// Use share so that the signal does not effect a
// second subscription to the source.
let signal;
const audited = source
.audit(() => signal)
.mergeMap(value => asyncTask(value))
.share();
// Compose a signal from the audited observable to
// which the async task is applied.
// Use startWith so that the first emitted value
// passes the audit.
signal = audited
.mapTo(true)
.startWith(true);
audited.subscribe(value => console.log("output", value));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
A Solution With RxJS v6+ Primatives
Here's a solution that works in current RxJS versions (now available as the Node package rxjs-audit-with):
export function auditWith<T, R>(
callback: (value: T) => Promise<any>,
): OperatorFunction<T, R> {
const freeToRun = new BehaviorSubject(true);
return (source: Observable<T>) => {
return source.pipe(
audit(_val => freeToRun.pipe(filter(free => free))),
tap(() => freeToRun.next(false)),
mergeMap(val => callback(val)),
tap(() => freeToRun.next(true)),
);
};
}
How It Works
auditWith is an operator that uses a BehaviorSubject to track whether callback is busy. It uses audit to keep only the most recent value to come down the pipe, and releases that value the next time the BehaviorSubject is marked as free. tap goes before and after the async call in order to make it as busy/free (respectively), and mergeMap is used to actually call the async callback. Note that callback receives the latest value from the pipe, but can return anything it wants (if anything at all); the same value will propagate down the pipe unchanged (by design).
An Example of Use
async function slowFunc(num: number) {
await new Promise(resolve =>
setTimeout(() => {
console.log("Processed num", num);
resolve(true);
}, 3000),
);
}
interval(500)
.pipe(
take(13),
auditWith(slowFunc),
)
.subscribe();
// Sample output:
// Processed num 0
// Processed num 5
// Processed num 11
// Processed num 12
Even though sequential numbers are generated by interval every 500ms, auditWith will hold the latest value in the pipe until slowFunc has returned from its previous iteration. Note that the very first value is guaranteed to be run (because the async function is assumed to start in the "available" state), and the last value in the Observable is also guaranteed to run (i.e. the pipe will drain).
use a combination of first() and repeat(). if a$ finished emission the sequence complete
//emit every 1s
const a$=new Rx.BehaviorSubject(0)
Rx.Observable.interval(1000).take(100).skip(1).subscribe(a$);
// //simulate aysnc
const async = (val)=>{
console.log('async start with:'+ val)
return Rx.Observable.timer(5100).mapTo('async done:'+val);
}
a$.first().switchMap(value=>async(value))
.repeat()
.catch(e=>Rx.Observable.empty())
.subscribe(console.log,console.err,console.warn)
a$.subscribe(console.warn)
https://jsbin.com/tohahod/65/edit?js,console

Toggling API call by a stream

Here:
import Rx from 'rxjs';
function fakeApi(name, delay, response) {
return new Rx.Observable(observer => {
console.log(`${name}: Request.`)
let running = true;
const id = setTimeout(() => {
console.log(`${name}: Response.`)
running = false;
observer.next(response);
observer.complete();
}, delay);
return () => {
if(running) console.log(`${name}: Cancel.`)
clearTimeout(id);
}
})
}
function apiSearch() { return fakeApi('Search', 4000, "This is a result of the search."); }
//============================================================
const messages$ = new Rx.Subject();
const toggle$ = messages$.filter(m => m === 'toggle');
const searchDone$ = toggle$.flatMap(() =>
apiSearch().takeUntil(toggle$)
);
searchDone$.subscribe(m => console.log('Subscriber:', m))
setTimeout(() => {
// This one starts the API call.
toggle$.next('toggle');
}, 2000)
setTimeout(() => {
// This one should only cancel the API call in progress, not to start a new one.
toggle$.next('toggle');
}, 3000)
setTimeout(() => {
// And this should start a new request again...
toggle$.next('toggle');
}, 9000)
my intent is to start the API call and stop it when it is in progress by the same toggle$ signal. Problem with the code is that toggle$ starts a new API call every time. I would like it not to start the new call when there is one already running, just to stop the one which is already in progress. Some way should I "unsubscribe" the outermost flatMap from toggle$ stream while apiSearch() is running. I guess that there is a need to restructure the code to achieve the behaviour... What is the RxJS way of doing that?
UPDATE: After some more investigations and user guide lookups, I came with this:
const searchDone$ = toggle$.take(1).flatMap(() =>
apiSearch().takeUntil(toggle$)
).repeat()
Works like it should. Still feels cryptic a little bit. Is this how you RxJS guys would solve it?
I think your solution will work only once since you're using take(1). You could do it like this:
const searchDone$ = toggle$
.let(observable => {
let pending;
return observable
.switchMap(() => {
let innerObs;
if (pending) {
innerObs = Observable.empty();
} else {
pending = innerObs = apiSearch();
}
return innerObs.finally(() => pending = null);
});
});
I'm using let() only to wrap pending without declaring it in parent scope. The switchMap() operator unsubscribes for you automatically without using take*().
The output with your test setTimeouts will be as follows:
Search: Request.
Search: Cancel.
Search: Request.
Search: Response.
Subscriber: This is a result of the search.

RxJs flatMapLatest/switchMap cancel callback. Where is onCancel()?

I've got 2 nested Observable Streams which do HTTP requests. Now I'd like to display a loading indicator, but can't get it working correctly.
var pageStream = Rx.createObservableFunction(_self, 'nextPage')
.startWith(1)
.do(function(pageNumber) {
pendingRequests++;
})
.concatMap(function(pageNumber) {
return MyHTTPService.getPage(pageNumber);
})
.do(function(response) {
pendingRequests--;
});
Rx.createObservableFunction(_self, 'search')
.flatMapLatest(function(e) {
return pageStream;
})
.subscribe();
search();
nextPage(2);
nextPage(3);
search();
This will trigger pendingRequests++ 4 times, but pendingRequests-- only once, because flatMapLatest will cancel the inner observable before the first 3 HTTP responses arrive.
I couldn't find anything like an onCancel callback. I also tried onCompleted and onError, but those too won't get triggered by flatMapLatest.
Is there any other way to get this working?
Thank you!
EDIT: Desired loading indicator behavior
Example: Single search() call.
search() -> start loading indicator
when search() response comes back -> disable loading indicator
Example: search() and nextPage() call. (nextPage() is called before search() response came back.)
search() -> start loading indicator
nextPage() -> indicator is already started, though nothing to do here
stop loading indicator after both responses arrived
Example: search(), search(). (search() calls override each other, though the response of the first one can be dismissed)
search() -> start loading indicator
search() -> indicator is already started, though nothing to do here
stop loading indicator when the response for the second search() arrived
Example: search(), nextPage(), search(). (Again: Because of the second search(), the responses from the previous search() and nextPage() can be ignored)
search() -> start loading indicator
nextPage() -> indicator is already started, though nothing to do here
search() -> indicator is already started, though nothing to do here
stop loading indicator when response for the second search() arrived
Example: search(), nextPage(). But this time nextPage() is called after search() response came back.
search() -> start loading indicator
stop loading indicator because search() response arrived
nextPage() -> start loading indicator
stop loading indicator because nextPage() response arrived
I tried using pendingRequests counter, because I can have multiple relevant requests at the same time (for example: search(), nextPage(), nextPage()). Then of course I'd like to disable the loading indicator after all those relevant requests finished.
When calling search(), search(), the first search() is irrelevant. Same applies for search(), nextPage(), search(). In both cases there's only one active relevant request (the last search()).
With switchMap aka flatMapLatest you want to trim asap execution of the current inner-stream as new outer-items arrive. It is surely a good design decision as otherwise it would bring a lot of confusion and allow some spooky actions. If you really wanted do do something onCancel you can always create your own observable with custom unsubscribe callback. But still I would recommend not to couple unsubscribe with changing state of the external context. Ideally the unsubscribe would only clean up internally used resources.
Nevertheless your particular case can be solved without accessing onCancel or similar. The key observation is - if I understood your use case correctly - that on search all previous / pending actions may be ignored. So instead of worry about decrementing the counter we can simply start counting from 1.
Some remarks about the snippet:
used BehaviorSubject for counting pending requests - as it is ready to be composed with other streams;
checked all cases you posted in your question and they work;
added some fuzzy tests to demonstrate correctness;
not sure if you wanted to allow nextPage when a search is still pending - but it seems to be just a matter of using concatMapTo vs merge;
used only standard Rx operators.
PLNKR
console.clear();
const searchSub = new Rx.Subject(); // trigger search
const nextPageSub = new Rx.Subject(); // triger nextPage
const pendingSub = new Rx.BehaviorSubject(); // counts number of pending requests
const randDurationFactory = min => max => () => Math.random() * (max - min) + min;
const randDuration = randDurationFactory(250)(750);
const addToPending = n => () => pendingSub.next(pendingSub.value + n);
const inc = addToPending(1);
const dec = addToPending(-1);
const fakeSearch = (x) => Rx.Observable.of(x)
.do(() => console.log(`SEARCH-START: ${x}`))
.flatMap(() =>
Rx.Observable.timer(randDuration())
.do(() => console.log(`SEARCH-SUCCESS: ${x}`)))
const fakeNextPage = (x) => Rx.Observable.of(x)
.do(() => console.log(`NEXT-PAGE-START: ${x}`))
.flatMap(() =>
Rx.Observable.timer(randDuration())
.do(() => console.log(`NEXT-PAGE-SUCCESS: ${x}`)))
// subscribes
searchSub
.do(() => console.warn('NEW_SEARCH'))
.do(() => pendingSub.next(1)) // new search -- ingore current state
.switchMap(
(x) => fakeSearch(x)
.do(dec) // search ended
.concatMapTo(nextPageSub // if you wanted to block nextPage when search still pending
// .merge(nextPageSub // if you wanted to allow nextPage when search still pending
.do(inc) // nexpage started
.flatMap(fakeNextPage) // optionally switchMap
.do(dec) // nextpage ended
)
).subscribe();
pendingSub
.filter(x => x !== undefined) // behavior-value initially not defined
.subscribe(n => console.log('PENDING-REQUESTS', n))
// TEST
const test = () => {
searchSub.next('s1');
nextPageSub.next('p1');
nextPageSub.next('p2');
setTimeout(() => searchSub.next('s2'), 200)
}
// test();
// FUZZY-TEST
const COUNTER_MAX = 50;
const randInterval = randDurationFactory(10)(350);
let counter = 0;
const fuzzyTest = () => {
if (counter % 10 === 0) {
searchSub.next('s' + counter++)
}
nextPageSub.next('p' + counter++);
if (counter < COUNTER_MAX) setTimeout(fuzzyTest, randInterval());
}
fuzzyTest()
<script src="https://npmcdn.com/rxjs#5.0.0-beta.11/bundles/Rx.umd.js"></script>
One way: use the finally operator (rxjs4 docs, rxjs5 source). Finally triggers whenever the observable is unsubscribed or completes for any reason.
I'd also move the counter logic to inside the concatMap function since you are really counting the getPage requests, not the number of values that have gone through. Its a subtle difference.
var pageStream = Rx.createObservableFunction(_self, 'nextPage')
.startWith(1)
.concatMap(function(pageNumber) {
++pendingRequests;
// assumes getPage returns an Observable and not a Promise
return MyHTTPService.getPage(pageNumber)
.finally(function () { --pendingRequests; })
});
I wrote a solution for your problem from scratch.
For sure it might be written in a more functional way but it works anyway.
This solution is based on reqStack which contains all requests (keeping the call order) where a request is an object with id, done and type properties.
When the request is done then requestEnd method is called.
There are two conditions and at least one of them is enough to hide a loader.
When the last request on the stack was a search request then we can hide a loader.
Otherwise, all other requests have to be already done.
function getInstance() {
return {
loaderVisible: false,
reqStack: [],
requestStart: function (req){
console.log('%s%s req start', req.type, req.id)
if(_.filter(this.reqStack, r => r.done == false).length > 0 && !this.loaderVisible){
this.loaderVisible = true
console.log('loader visible')
}
},
requestEnd: function (req, body, delay){
console.log('%s%s req end (took %sms), body: %s', req.type, req.id, delay, body)
if(req === this.reqStack[this.reqStack.length-1] && req.type == 'search'){
this.hideLoader(req)
return true
} else if(_.filter(this.reqStack, r => r.done == true).length == this.reqStack.length && this.loaderVisible){
this.hideLoader(req)
return true
}
return false
},
hideLoader: function(req){
this.loaderVisible = false
console.log('loader hidden (after %s%s request)', req.type, req.id)
},
getPage: function (req, delay) {
this.requestStart(req)
return Rx.Observable
.fromPromise(Promise.resolve("<body>" + Math.random() + "</body>"))
.delay(delay)
},
search: function (id, delay){
var req = {id: id, done: false, type: 'search'}
this.reqStack.push(req)
return this.getPage(req, delay).map(body => {
_.find(this.reqStack, r => r.id == id && r.type == 'search').done = true
return this.requestEnd(req, body, delay)
})
},
nextPage: function (id, delay){
var req = {id: id, done: false, type: 'nextPage'}
this.reqStack.push(req)
return this.getPage(req, delay).map(body => {
_.find(this.reqStack, r => r.id == id && r.type == 'nextPage').done = true
return this.requestEnd(req, body, delay)
})
},
}
}
Unit tests in Moca:
describe('animation loader test:', function() {
var sut
beforeEach(function() {
sut = getInstance()
})
it('search', function (done) {
sut.search('1', 10).subscribe(expectDidHideLoader)
testDone(done)
})
it('search, nextPage', function (done) {
sut.search('1', 50).subscribe(expectDidHideLoader)
sut.nextPage('1', 20).subscribe(expectDidNOTHideLoader)
testDone(done)
})
it('search, nextPage, nextPage', function(done) {
sut.search('1', 50).subscribe(expectDidHideLoader)
sut.nextPage('1', 40).subscribe(expectDidNOTHideLoader)
sut.nextPage('2', 30).subscribe(expectDidNOTHideLoader)
testDone(done)
})
it('search, nextPage, nextPage - reverse', function(done) {
sut.search('1', 30).subscribe(expectDidNOTHideLoader)
sut.nextPage('1', 40).subscribe(expectDidNOTHideLoader)
sut.nextPage('2', 50).subscribe(expectDidHideLoader)
testDone(done)
})
it('search, search', function (done) {
sut.search('1', 60).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
sut.search('2', 50).subscribe(expectDidHideLoader)
testDone(done)
})
it('search, search - reverse', function (done) {
sut.search('1', 40).subscribe(expectDidNOTHideLoader)
sut.search('2', 50).subscribe(expectDidHideLoader)
testDone(done)
})
it('search, nextPage, search', function (done) {
sut.search('1', 40).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
sut.nextPage('1', 30).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
sut.search('2', 10).subscribe(expectDidHideLoader)
testDone(done)
})
it('search, nextPage (call after response from search)', function (done) {
sut.search('1', 10).subscribe(result => {
expectDidHideLoader(result)
sut.nextPage('1', 10).subscribe(expectDidHideLoader)
})
testDone(done)
})
function expectDidNOTHideLoader(result){
expect(result).to.be.false
}
function expectDidHideLoader(result){
expect(result).to.be.true
}
function testDone(done){
setTimeout(function(){
done()
}, 200)
}
})
Part of the output:
JSFiddle is here.
I think there's a much simpler solution, to explain it I'd like to "re-phrase" the examples you gave in your edit:
The status is "pending" as long as there are un-closed requests.
A response closes all previous requests.
Or, in stream/marbles fashion
(O = request [open], C = response [close], p = pending, x = not pending)
http stream: ------O---O---O---C---O---C---O---O---C---O---
------ Status: x----P--------------x---P----x----P---------x---P---
You can see that the count doesn't matter, we have a flag which is actually on (pending) or off (a response was returned). This true because of you switchMap/flatMap, or as you said at the end of your edit, at each time there's only one active request.
The flag is actually a boolean observable/oberver or simply a subject.
So, what you need is to first define:
var hasPending: Subject<boolean> = BehaviorSubject(false);
The BehaviorSubject is relevant for 2 reasons:
You can set an initial value (false = nothing pending).
New subscribers get the last value, hence, even components that are created later will know if you have a pending request.
Than the rest becomes simple, whenever you send out a request, set the pending to 'true', when a request is done, set the pending flag to 'false'.
var pageStream = Rx.createObservableFunction(_self, 'nextPage')
.startWith(1)
.do(function(pageNumber) {
hasPending.next(true);
})
.concatMap(function(pageNumber) {
return MyHTTPService.getPage(pageNumber);
})
.do(function(response) {
hasPending.next(false);
});
Rx.createObservableFunction(_self, 'search')
.flatMapLatest(function(e) {
return pageStream;
})
.subscribe();
This is rxjs 5 syntax, for rxjs 4 use onNext(...)
If you don't need your flat as an observable, just a value, just declare:
var hasPending: booolean = false;
Then in the .do before the http call do
hasPending = true;
and in the .do after the http call do
hasPending = false;
And that's it :-)
Btw, after re-reading everything, you can test this by an even simpler (though somewhat quick and dirty) solution:
Change your post http 'do' call to:
.do(function(response) {
pendingRequests = 0;
});

Categories