steveareeno
steveareeno

Reputation: 1977

Checking which Angular signals caused effect

Is there any way in an effect that I can determine which Angular signal caused an effect to execute? I have two signals and one effect in a component. For one signal, I want to start a timer function within a service. The other signal is based on the changing conditions in the service timer function and can change frequently.

I have a workaround solution but it would be useful to know which signal caused the effect. The work around is to set an isTimerRunning boolean value in the service when the timer starts.

Here is my code:

import { Injectable, Signal, WritableSignal, signal } from '@angular/core';
import { Observable, Subscription, merge, fromEvent, timer, BehaviorSubject } from 'rxjs';
import { environment } from '../../environments/environment';
import { LocalStorageService } from './storage.service';

export interface IIdleTimeoutModel {
  expired: boolean;
  expiring: boolean;
}

@Injectable({
  providedIn: 'root',
})

export class IdleTimeoutService {
  constructor(private localStorageService: LocalStorageService) { }

  idleTimeout: IIdleTimeoutModel = { expired: false, expiring: false }
  private idleSignal: WritableSignal<IIdleTimeoutModel> = signal(this.idleTimeout);
  readonly idleSignalModel: Signal<IIdleTimeoutModel> = this.idleSignal.asReadonly();
  
  private idleEventTriggers: Observable<any> = new Observable();
  private timer: Subscription = new Subscription();
  private timeOutMilliSeconds: number = 1000;
  private timeOutWarningMilliSeconds: number = 1000;
  private isTimerRunning: boolean = false;

  public startWatching(): void {
    if (!this.isTimerRunning) {
      this.timeOutMilliSeconds = environment.idleTimeInMilliseconds;
      this.timeOutWarningMilliSeconds = environment.idleTimeWarningInMilliseconds;
  
      this.idleEventTriggers = merge(
        fromEvent(document, 'click'),
        fromEvent(document, 'mousedown'),
        fromEvent(document, 'keypress')
      );
  
      this.idleEventTriggers.subscribe((res) => {
        this.resetTimer();
      });
  
      this.startTimer();
     }

  }

  private startTimer() {
    this.isTimerRunning = true; // <-- this is what I am currently doing to prevent the signal from staring the timer multiple times
    let timeoutModel: IIdleTimeoutModel = {
      expired: false,
      expiring: false,
    };
    let initDatetime = new Date();
    let timeoutSeconds = this.timeOutWarningMilliSeconds / 1000 + initDatetime.getSeconds();
    let warningSeconds = this.timeOutMilliSeconds / 1000 + initDatetime.getSeconds();
    let expiringDatetime = new Date().setSeconds(timeoutSeconds);
    let expiredDatetime = new Date().setSeconds(warningSeconds);

    this.localStorageService.set('expiringDatetime', expiringDatetime.toString());
    this.localStorageService.set('expiredDatetime', expiredDatetime.toString());

    // timer
    this.timer = timer(1000, 5000).subscribe((response) => {

      let nowDatetime = new Date();

      // expiringDatetime
      let checkExpiringDatetimeValue = this.localStorageService.get('expiringDatetime');
      if (checkExpiringDatetimeValue) {
        if (nowDatetime.getTime() >= parseInt(checkExpiringDatetimeValue)) {
          this.idleSignal.set({ expiring: true, expired: false })
        } else {
          this.idleSignal.set({ expiring: false, expired: false })
        }
      } else {
        this.resetTimer();
      }

      // expiredDatetime
      let checkExpiredDatetimeValue = this.localStorageService.get('expiredDatetime');
      if (checkExpiredDatetimeValue) {
        if (nowDatetime.getTime() >= parseInt(checkExpiredDatetimeValue)) {
          if (timeoutModel.expired === false) {
            timeoutModel.expired = true;
            this.idleSignal.set({ expiring: false, expired: true })
          }
        } else {
          if (timeoutModel.expired === true) {
            timeoutModel.expired = false;
            this.idleSignal.set({ expiring: false, expired: true })
          }
        }
      } else {
        this.resetTimer();
      }
    });
  }

  public resetTimer() {
    this.timer.unsubscribe();
    this.idleSignal.set({ expiring: false, expired: false })
    this.startTimer();
  }

  public stopTimer() {
    this.isTimerRunning = false;
    this.timer.unsubscribe();
  }
}

Component (app component) :

// Variables declare in the component:

  isLoggedIn = this.appService.isLoggedInSignal; // this is a signal in our app service that is set when a user log in
  private idleTimeoutSignalModel = this.idleTimeoutService.idleSignalModel; // this is a signal from the IdleTimeoutService service above. this will change based on the timer function


In the constructor:
    effect(() => {
      if (this.isLoggedIn() && environment.idleTimeout) {
        const idleModel = this.idleTimeoutSignalModel();
        
        this.idleTimeoutService.startWatching(); // <-- this is the function I want to only run once based on isLoggedIn signal chnaging
        
        if (idleModel.expiring) {
          this.dialogService.openTimeOutDialog('Extend Session?', 'Due to inactivity, your login session will expire shortly. Do you want to continue?');
        } else {
          this.dialogService.closeDialogById('timeout-dialog');
        }

        if (idleModel.expired) {
          try {
            this.broadcastService.publish({
              type: 'mnsso-logout',
              payload: 'true',
            });
          } catch {
            console.log('broadcast error');
          }
          this._authService.logout();
        }
      }
    });

Upvotes: 2

Views: 2376

Answers (2)

steveareeno
steveareeno

Reputation: 1977

I found a solution. You can have more than one effect in a component. Each effect will only be triggered by the signal referenced in the effect. So I can rewrite my code as follows:

isLoggedIn = this.appService.isLoggedInSignal; 
private idleTimeoutSignalModel = this.idleTimeoutService.idleSignalModel; 

    // will only be triggered by this.isLoggedIn()
    effect(() => {
      if (this.isLoggedIn() && environment.idleTimeout) { //<-- this.isLoggedIn() signal
        this.idleTimeoutService.startWatching();
      }
    })

   // will only be triggered by isLoginExpiring() and isLoginExpired()
    effect(() => {
      if (environment.idleTimeout) {
        if (this.idleTimeoutService.isLoginExpiring()) { // <-- isLoginExpiring() signal
          this.dialogService.openTimeOutDialog('Extend Session?', 'Due to inactivity, your login session will expire shortly. Do you want to continue?');
        } else {
          this.dialogService.closeDialogById('timeout-dialog');
        }

        if (this.idleTimeoutService.isLoginExpired()) { // <-- isLoginExpired() signal
          try {
            this.broadcastService.publish({
              type: 'mnsso-logout',
              payload: 'true',
            });
          } catch {
            console.log('broadcast error');
          }
          this.authService.logout();
        }
      }
    });

The first effect only listens to the isLoggedIn() signal because it is in the if statement.

The second effect only listens to the this.idleTimeoutService.isLoginExpiring() and this.idleTimeoutService.isLoginExpired() signals.

in other words, when you have any code in an effect that references a signal, that signal will cause the effect to trigger. Any signal in the component that is not referenced in an effect will not trigger the effect

Upvotes: 1

Matthieu Riegler
Matthieu Riegler

Reputation: 55554

Effects are fired when their reactive node is marked as dirty.

At the time of writing there is no tracking of which signal makes another one dirty.

So there is no way to determine which signal triggers an effect.

Upvotes: 2

Related Questions