-1

I'm trying to accomplish the following: for my web app I tried implementing a timer which automatically resets back to 60s when the user has shown activity (meaning triggered either a click, keydown or scroll event). I used Observables for the Event Streams. All of this happens AFTER you login into my web app.

If the timer runs out (goes from 60s to 0) the user should simply be redirected to the backend URL, which deletes the cookie and redirects back to the Frontend's login screen. Now it's all nice and dandy, the timeout works but when I click something, scroll or click something on my keyboard nothing happens. I'd also like to have an event stream which detects mouse movement, how do I do that with RxJS?

My code:

Data Service:

// login timer
 public timeLeft = 60;
 public interval: number;

  constructor(private http: HttpClient, public datepipe: DatePipe, private cookieService: CookieService) {
  }

  public startTimer() {
    this.interval = setInterval(() => {
      if(this.timeLeft > 0) {
        this.timeLeft--;
      } else {
        // FIXME HTTPS implementation
        window.location.href = 'http://example.com:8081/logout';
        clearInterval(this.interval);
        this.timeLeft = 60;
      }
    }, 1000);
  }

  public resetTimer() {
    console.log('Juup');
    clearInterval(this.interval);
    console.log(this.timeLeft);
    //this.timeLeft = 60;
    //this.startTimer();
    /* let time;
    clearTimeout(time);
    time = setTimeout(this.logout, 3000); */
  }

Login logic:

return this.http.post<any>(`${environment.apiUrl}/api/login`, { email, password }, {withCredentials: true})
        .pipe(map(user => {
// login logic here
// if everything is ok then
this.DataService.startTimer();

app.component.ts:

constructor(public authenticationService: AuthenticationService, public DataService: GlobalDataService,
              private router: Router) {
    const source1$ = fromEvent(document, 'click');
    const source2$ = fromEvent(document, 'keydown');
    const source3$ = fromEvent(document, 'scroll');
    const sources$ = merge (
    source1$,
    source2$,
    source3$
  );
// map to string with given event timestamp
    const example = sources$.pipe(map(event => `Event time: ${event.timeStamp}`));
// output (example): 'Event time: 7276.390000000001'
//
// const subscribe = sources$.subscribe(val => console.log(val));
    const subscribe = sources$.subscribe(this.DataService.resetTimer);

interval: number;
subscribeTimer: any;

observableTimer() {
    const source = timer(1000, 2000);
    const abc = source.subscribe(val => {
      console.log(val, '-');
      this.subscribeTimer = this.DataService.timeLeft - val;
    });
  }

My app.component.html: <p>{{this.DataService.timeLeft}}</p>

My errors:

None.

Misc. console output:

The console.log prints out the Juup once and then prints out undefined for the timeLeft. It happens every time I click.

Munchkin
  • 857
  • 5
  • 24
  • 51

1 Answers1

0

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

  1. 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.

  1. 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.

  1. 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

ruth
  • 29,535
  • 4
  • 30
  • 57
  • 1
    Why are identical two event emitters from mouseup getting merged? – Munchkin Oct 15 '20 at 15:04
  • @Munchkin: It was supposed to be `mouseup` and `mousemove`. I overlooked it. I've edited the answer. The events here are only for illustration. – ruth Oct 15 '20 at 15:06
  • @Munchkin: Hold on please, the `finalize` is not being triggered at the end of the countdown. – ruth Oct 15 '20 at 15:17
  • @Munchkin: I've updated the answer to replace `finalize` with `tap`'s `complete` callback. Please note the difference. – ruth Oct 15 '20 at 15:34
  • Is it a good practice to have the countDown$ variable defined as any type? – Munchkin Oct 16 '20 at 08:30
  • 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? – Munchkin Oct 16 '20 at 08:38
  • 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. – Munchkin Oct 16 '20 at 08:43
  • @Munchkin: I've updated the answer along with the answers to your questions. Please see if it works for you. – ruth Oct 16 '20 at 10:34