Reputation: 101
I have a simple stopwatch, with using rxjs
Problem is: can't get how to pause a stream of my interval, and then continue it stackbiz
Upvotes: 1
Views: 2838
Reputation: 8022
I've seen stopwatch questions come up often enough that I figured it would be interesting to create a custom stopWatch observable. The RxJS way would be to implement this by switching into and out of timers/intervals.
Another interesting way to implement this is by using setTimeout instead. setTimeout should actually require a bit less memory as we're not leaning on the observable apparatus to accomplish our timing goals
How will this work? Our custom observable creates a stream that outputs the number on the stopwatch and is controlled by a separate stream (Here called control$
). So when control$
emits "START", the stopWatch starts, when it emits "STOP", the stopwatch stops, and when it emits "RESET" the stopwatch sets the counter back to zero. When control$
errors or completes, the stopwatch errors or completes.
function createStopwatch(control$: Observable<string>, interval = 1000): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const endTicker$ = new Subject();
const ticker = () => {
return timer(0, interval).pipe(
takeUntil(endTicker$),
map(x => count++)
)
}
return control$.pipe(
tap({
next: _ => {/*Do nothing*/},
complete: () => {
endTicker$.next();
endTicker$.complete();
},
error: err => {
endTicker$.next();
endTicker$.complete();
}
}),
filter(control =>
control === "START" ||
control === "STOP" ||
control === "RESET"
),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker();
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker();
}
}
return EMPTY;
})
);
});
}
function createStopwatch(control: Observable<string>, interval = 1000): Observable<number> {
return new Observable(observer => {
let count: number = 0;
let tickerId: number = null;
const clearTicker = () => {
if(tickerId != null){
clearTimeout(tickerId);
tickerId = null;
}
}
const setTicker = () => {
const recursiveTicker = () => {
tickerId = setTimeout(() => {
observer.next(count++);
recursiveTicker();
}, interval);
}
clearTicker();
observer.next(count++);
recursiveTicker();
}
control.subscribe({
next: input => {
if(input === "START" && tickerId == null){
setTicker();
}else if(input === "STOP"){
clearTicker();
}else if(input === "RESET"){
count = 0;
if(tickerId != null){
setTicker();
}
}
},
complete: () => {
clearTicker();
observer.complete();
},
error: err => {
clearTicker();
observer.error(err);
}
});
return {unsubscribe: () => clearTicker()};
});
}
Here is an example of this observable being used. I manage the control stream via a subject, but it could just as easily be merged/mapped DOM events or somesuch.
const control$ = new Subject<string>();
createStopwatch(control$, 250).subscribe(console.log);
// We send a new action to our control stream every 1 second
const actions = ["START", "STOP", "START", "RESET", "START"]
zip(from(actions), interval(1000)).pipe(
map((x,y) => x),
finalize(() => {
// After 5 seconds, unsubscribe via the control
// If our control finishes in any way (
// completes, errors, or is unsubscribed), our
// sopwatch reacts by doing the same.
control$.complete();
})
).subscribe(x => control$.next(x));
This controls the stopwatch with setTimeout
instead of interval
.
const control$ = new Subject<string>();
createStopwatch(control$, 250).subscribe(console.log);
// We send a new action to our control stream every 1 second
const actions = ["START", "STOP", "START", "RESET", "START"]
actions.forEach((val, index) => {
setTimeout(() => {
control$.next(val);
},
index * 1000);
})
// Unsubscribe via the control
setTimeout(() => {
control$.complete();
}, actions.length * 1000);
Upvotes: 1
Reputation: 1847
Take a look at my solution in this stackblitz
The component has to subscribe to the StopWatch-Service. I do not like the idea of giving a service a value and then the service CHANGES the value implicitly. Therefor i work with an explicit way to get the updated stopWatch (as an observable).
I work with "timer" instead of "interval", because "interval" would emit the first value (a 0) after a second, therefor my stopWath would have a delay,
There is a little trick. There is a private variable "timer$", a BehaviorSubject. And as soon as the counter is started, i start a timer, and subscribe to it. In that subscription each emit of the timer, will emit a new value for the BehaviorSubject.
Now i also store the subscription.
If i want to stop everything, i just unsubscribe from the "timer". As a result the clock stops. But because it is kind of decoupled from the behaviorSubject timer$
that one will still have stored the last value.
Or to say it differently:
The BehaviorSubject lives for ever, the consumer is never unsubscribed (only if he does it himself). And i kind of "attach" and "unattach" the timer function to it, whenever the counting should start or stop.
By the way, its a good habit to always make sure that your component will unsubscribe from all running observables, when the component is destroyed. If that is not done, than those still active subscriptions may result in performance or even worse problems in an application.
i hope it helps a bit
warm regards
Upvotes: 2