Reputation: 1977
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
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
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