I didn't look into your exact problem. But here is a very basic implementation of a countdown timer that is reset every time user clicks or moves the mouse anywhere in the app.
import { Component, OnInit } from "@angular/core";
import { timer, Observable, fromEvent, merge, Subject } from "rxjs";
import { startWith, switchMap, finalize, take, map } from "rxjs/operators";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
countDown$: Observable<any>;
ngOnInit() {
this.countDown$ = merge(
fromEvent(document, "mouseup"),
fromEvent(document, "mousemove")
).pipe(
startWith(false),
switchMap(_ => {
let counter = 61;
return timer(0, 1000).pipe(
take(counter),
map(_ => --counter),
tap(null, null, () => {
console.log("countdown complete");
// redirect to login page
})
);
})
);
}
}
Working example: Stackblitz
Working example: Stackblitz
Breakdown
fromEvent
- Used to capture the mousedown
and mouseup
events. You could adjust it to a specific part of the screen using Angular template reference variables or add/remove additional events.
merge
- Used to merge multiple events to a single event
startWith
- Used to emit a random variable before any of the event emits. Used to kickstart the countdown at the start of the app.
switchMap
- Higher order mapping operator used to cancel the current inner observable (the timer
here) when the source observable (merge
here) emits.
timer
- Used to emit a value every second.
map
- Transform the value from timer
to our countdown timer value.
finalize
- Will be triggered only when the timer
completes. In other words, as long the source observables keep emitting within the 60 seconds, it won't be triggered. So you could include the routing mechanism here.
Update - finalize
doesn't work as expected
It appears each event in the merge
possibly seeps into the finalize
operator since each of them individually appear to complete before the modified observable from switchMap
. I've replaced it with tap
operator's complete block that works as expected. The above explanation for finalize
should still work, but replaced with tap
instead
tap
- It's complete
callback will be triggered only when the timer
completes. In other words, as long the source observables keep emitting within the 60 seconds, it won't be triggered. So you could include the routing mechanism here.
Update: use countdown with an authentication service
I've updated the illustration completely to work in tandem with an authentication service that simulates HTTP calls for login and logout actions.
Regarding your questions in the comment section
- Is it a good practice to have the
countDown$
variable defined as any
type?
In short, no. You could use Observable<number>
instead since it holds the number emitted from the observable.
- Also I get the following warning of deprecation from the tap operator:
tap is deprecated: Use an observer instead of a complete callback (deprecation)
How do I fix the deprecation?
tap
isn't deprecated. Most probably it's taking about the function overload that is deprecated. You could use an object with next
, error
and complete
properties instead.
- Btw I require to start the timer only after logging in, how do I accomplish this? In my code I just use the
startTimer()
call in the authentication service.
Hopefully the updated illustration suffices.
app.component.ts
import { Component, OnDestroy } from "@angular/core";
import { fromEvent, merge, Subject } from "rxjs";
import { switchMap, takeUntil } from "rxjs/operators";
import { AuthenticationService } from "./authentication.service";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnDestroy {
closed$ = new Subject<any>();
countDown: number;
constructor(private authService: AuthenticationService) {}
login() {
this.authService
.login()
.pipe(
switchMap(_ =>
merge(
fromEvent(document, "mouseup"), // <-- replace these with user events to reset timer
fromEvent(document, "mousemove")
)
),
this.authService.countdownTimer(),
takeUntil(this.closed$)
)
.subscribe(countDown => (this.countDown = countDown));
}
logout() {
this.authService
.logout()
.pipe(takeUntil(this.closed$))
.subscribe();
}
ngOnDestroy() {
this.closed$.next();
}
}
app.component.html
<ng-container *ngIf="(authService.loginStatus$ | async); else loggedOut">
<p>User is logged in. Automatic logout in: {{ countDown }}s</p>
<br />
<button (mouseup)="logout()">Logout</button>
</ng-container>
<ng-template #loggedOut>
<p>User is logged out.</p>
<br />
<button (mouseup)="login()">Login</button>
</ng-template>
authentication.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { timer, Observable, Subject, BehaviorSubject } from "rxjs";
import {
tap,
startWith,
switchMap,
takeUntil,
take,
map
} from "rxjs/operators";
export const LOGIN_TIME = 6;
@Injectable()
export class AuthenticationService {
public loginStatusSrc = new BehaviorSubject<boolean>(false);
public stopTimerSrc = new Subject<any>();
public loginStatus$ = this.loginStatusSrc.asObservable();
public stopTimer$ = this.stopTimerSrc.asObservable();
constructor(private http: HttpClient) {}
login(): Observable<any> {
// simulate a login HTTP call
return this.http.get("https://jsonplaceholder.typicode.com/users/1").pipe(
tap({
next: () => this.loginStatusSrc.next(true),
error: () => this.loginStatusSrc.next(false)
})
);
}
logout() {
// simulate a logout HTTP call
return this.http.get("https://jsonplaceholder.typicode.com/users/1").pipe(
tap({
next: () => {
this.loginStatusSrc.next(false); // <-- hide timer
this.stopTimerSrc.next(); // <-- stop timer running in background
},
error: () => this.loginStatusSrc.next(false)
})
);
}
countdownTimer() {
return <T>(source: Observable<T>) => {
return source.pipe(
startWith(null),
switchMap(_ => {
let counter = LOGIN_TIME;
return timer(0, 1000).pipe(
take(counter),
map(_ => --counter),
tap({
next: null,
error: null,
complete: () => {
this.stopTimerSrc.next(); // <-- stop timer in background
this.loginStatusSrc.next(false);
console.log("Countdown complete. Rerouting to login page...");
// redirect to login page
}
})
);
}),
takeUntil(this.stopTimer$)
);
};
}
}
Here is the guide I used to create the custom RxJS operator countdownTimer()
: https://netbasal.com/creating-custom-operators-in-rxjs-32f052d69457
Working example: Stackblitz