I am making a table that will update every "x" seconds. Where x can be selected by the user. The request being made depends on a filter, also selected by the user. So if the user selects 10 seconds and filters for data from a certain day, a request will be made in 10 seconds, wait for the response, and then start the timer again (this is important as well, the timer must only reset once the request completes).
I have managed to achieve this, but what I am struggling with is displaying how long until the next request, like a countdown.
Here is what I have so far in my service:
private dataIntervalSubject = new BehaviourSubject<number>(5);
private dataLoadingSubject = new BehaviourSubject<boolean>(false);
dataLoadingChanged$ = this.dataLoadingSubject.asObservable();
constructor(
private globalFiltersService: GlobalFiltersService,
private http: HttpClient
) {
this.globalFiltersService.refreshIntervalSelectedAction$.subscribe(interval => {
this.dataIntervalSubject.next(interval);
});
}
dataTimer$ = this.dataIntervalSubject.pipe(
switchMap(val => interval(val * 1000))
);
data$ = combineLatest(
this.dataTimer$,
this.globalFiltersService.dataSelectedAction$
).pipe(
tap(() => this.dataLoadingSubject.next(true)),
switchMap(([interval, date]) => this.http.get(`dataUrl/${date}`)
.pipe(
tap(() => this.dataLoadingSubject.next(false))
)
)
);
And then in my component I am making use of the async pipe and have these component variables:
dataLoading$ = this.dataService.dataLoadingChanged$;
dataRequestTimeRemaining$ = this.dataService.dataTimer$;
data$ = this.dataService.data$;
(excuse the poor naming, these aren't the actual names, just simplified for the question)
What I have works, the only thing is the timer. Obviously the dataRequestTimeRemaining$ doesn't work in it's current state, it just shows a count of how many requests have been made.
I am unsure of how to adjust what I have in order to get a timer counting down how long until the next request. Any suggestions?
CodePudding user response:
First, I would make the Countdown it's own component. There's probably a lot out there already. Here's a basic implementation of a countdown stream you could use in a new component or, if you must, in your existing one.
private readonly countdownStartSubject = new Subject<{ duration: number, startDate: number }>();
@Input()
// this is an object as a cheezy way to have changes.
set startInfo(value: { duration: number }) {
this.countdownStartSubject.next(new Date().valueOf() value.duration);
}
// bind this to UI with async pipe.
const countdown$ = countdownStartSubject.pipe(
switchMap((endTime) => {
const stopper$ = timer(Math.max(0, endTime - new Date().valueOf()));
return concat(
interval(1000).pipe(
map(() => endTime - new Date().valueOf()),
takeUntil(stopper$)
),
of(0) // make sure this ends at 0.
)
})
);
So from your main component just create another stream that whose value will bound to the countdown component. This works similarly to dataTimer$ except that it emits a value at the start.
countdownStartInfo$ = this.dataIntervalSubject.pipe(
map(val => val * 1000),
switchMap(valMs => timer(0, valMs).pipe(map(() => ({ duration: valMs })))
);
CodePudding user response:
I suggest the following code. I tried to keep as close as possible to your code. Please see the comments for explanation. The main points of my solution are the following:
concat
to run countdown and request one after the other.repeat
to, well repeat, the procedure after success.filter
to supress emissions from the timer - it will trigger another observable and complete, but should never emit a value.
I believe the usage of the other operators should be clear, if not ask please.
import { BehaviorSubject, concat, concatMap, delay, filter, finalize, Observable, of, repeat, Subject, switchMap, takeWhile, tap, timer } from 'rxjs';
// will emit wether or not data is currently loading.
const dataLoadingSubject$ = new Subject<boolean>();
// mock of the actual api call, just get some data with some delay.
function http(): Observable<any> {
return of({ myValue: 42 }).pipe(delay(500));
}
// wrapper for the actual http request that invokes dataLoadingSubject before and after.
function doRequest(): Observable<any> {
// arbitrary value to start the pipe.
return of(0).pipe(
// notify load start
tap(() => dataLoadingSubject$.next(true)),
// wait for actual request.
concatMap(() => http()),
// notify end of loading. Finalize should be used to handle errors correctly.
finalize(() => dataLoadingSubject$.next(false))
);
}
// will emit the countdown, 5, 4, 3, ...
const dataRequestTimeRemaining$ = new Subject<number>();
// creates an Observable that will count from interval to 0, then complete.
function countdown(interval: number): Observable<number> {
// timer() will emit an incrementing number each second
return timer(0, 1000).pipe(
// i is emitted by timer(), basically the number of elapsed seconds.
takeWhile((i) => i <= interval),
// trigger output in other Observable
tap((i) => dataRequestTimeRemaining$.next(interval - i)),
// supress any emission, we don't want them in the main pipe.
filter(() => false)
);
}
// will emit if the user selects a different interval.
const interval$ = new BehaviorSubject<number>(5);
// Outputs the loaded data.
const data$ = interval$.pipe(
switchMap((interval) => concat( // restart all, if interval changes
countdown(interval), // first run countdown
doRequest() // when countdown completes, start the request
).pipe(repeat({ delay: 0 })) // repeat countdown request when finished
)
);
// subscribe to everything
// instead of loggig you'd probably use an async-pipe
dataLoadingSubject$.subscribe((loading) => console.log(loading ? 'loading' : 'not loading'));
dataRequestTimeRemaining$.subscribe((t) => console.log(`time until request: ${t}s...`));
data$.subscribe(console.log);
Output:
time until request: 5s...
time until request: 4s...
time until request: 3s...
time until request: 2s...
time until request: 1s...
time until request: 0s...
loading
{ myValue: 42 }
not loading
time until request: 5s...
time until request: 4s...
[and so on]
This has the following properties:
- The request will be fired only after the countdown ends.
- The pipe will wait for the request to finish before repeating.
- It will repeat endlessly.
- The pipe will restart over whenever the interval changes.
- The main pipe will only emit the data received from the request, it won't emit the countdown itself.