3

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();
        }
      }
    });
3
  • please share the workaround, what if the users share the workaround back to you? Commented Jan 23, 2024 at 13:26
  • As for today you can determine it only by using an effect with only one signal. IE: You have one effect for signal A and you have another effect for all the others signal. Commented Jan 23, 2024 at 13:31
  • Naren, I will update my post with a code example this afternoon. Commented Jan 23, 2024 at 14:02

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

ok. that is what I thought because I couldn't find anything like that in the documentation. When I get time today I will update my question to include the code for what I am trying to do.
1

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

1 Comment

note that you can also use untracked(()=> mySignal()) within an effect to prevent that signal from triggering the effect

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.