Created
June 30, 2020 14:07
-
-
Save tw3/52822f726a98c4397073bfb0765d5882 to your computer and use it in GitHub Desktop.
Service that creates and maintains an alarm that can trigger session timeout warnings and session expiration events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@angular/core'; | |
import { | |
EMPTY, | |
merge as observableMerge, | |
Observable, | |
of as observableOf, | |
Subject, | |
timer as observableTimer | |
} from 'rxjs'; | |
import { delay, expand, map, takeUntil, takeWhile } from 'rxjs/operators'; | |
export enum AlarmType { | |
WARN, | |
EXPIRE | |
} | |
export interface AlarmEvent { | |
type: AlarmType; | |
secsLeft: number; | |
} | |
/** | |
* This service was created to provide a more accurate alarm/timer for session timeout. | |
* | |
* Timers in JavaScript are not guaranteed to execute at an exact time. Closing and later re-opening one's | |
* laptop lid complicates matters. As a result when the warning alarm is triggered it is necessary to compare its | |
* expected trigger time with the current time to determine how much time is really left before session expiration and | |
* whether a warning dialog is really needed. | |
* | |
* Reference: https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript | |
* Reference: https://blog.angularindepth.com/rxjs-understanding-expand-a5f8b41a3602 | |
*/ | |
@Injectable() | |
export class SessionExpireAlarmService { | |
private readonly ONE_SECOND = 1000; | |
private sessionExpireDate: Date; | |
private sessionExpireEpoch: number; | |
private sessionWarnStartDate: Date; | |
private alarmSlayer$: Subject<void>; | |
constructor() { | |
} | |
getAlarm(sessionExpireEpoch: number, warnDurationMsecs: number): Observable<AlarmEvent> { | |
this.slayPreviousAlarm(); | |
this.setSessionExpireDate(sessionExpireEpoch); | |
this.setSessionWarnDate(warnDurationMsecs); | |
// Get the warn and expire timers and merge them | |
const warn$: Observable<AlarmEvent> = this.getWarn$(); | |
const expire$: Observable<AlarmEvent> = this.getExpire$(); | |
const warnExpire$: Observable<AlarmEvent> = observableMerge(warn$, expire$); | |
// Add the ability to halt this alarm in the future when getting a new alarm | |
this.addAlarmSlayer(warnExpire$); | |
return warnExpire$; | |
} | |
private setSessionExpireDate(sessionExpireEpoch: number): void { | |
this.sessionExpireEpoch = sessionExpireEpoch; | |
this.sessionExpireDate = new Date(sessionExpireEpoch); | |
} | |
private setSessionWarnDate(warnDurationMsecs: number): void { | |
const sessionWarnEpoch: number = this.sessionExpireEpoch - warnDurationMsecs; | |
this.sessionWarnStartDate = new Date(sessionWarnEpoch); | |
} | |
private getWarn$(): Observable<AlarmEvent> { | |
return observableTimer(this.sessionWarnStartDate).pipe( | |
expand(() => { | |
const msecsLeft: number = this.getMsecsBeforeExpire(); | |
if (msecsLeft <= 0) { | |
return EMPTY; | |
} | |
return observableOf(undefined).pipe(delay(this.getNextWarnEventDelay(msecsLeft))); | |
}), | |
map(() => this.getMsecsBeforeExpire()), | |
takeWhile(msecsLeft => msecsLeft > 0), | |
map(msecsLeft => { | |
const secsLeft: number = Math.ceil(msecsLeft / this.ONE_SECOND); | |
return { | |
type: AlarmType.WARN, | |
secsLeft | |
}; | |
})); | |
} | |
private getExpire$(): Observable<AlarmEvent> { | |
return observableTimer(this.sessionExpireDate).pipe( | |
map(() => { | |
const secsLeft = 0; | |
return { | |
type: AlarmType.EXPIRE, | |
secsLeft | |
}; | |
})); | |
} | |
private addAlarmSlayer(warnExpire$: Observable<AlarmEvent>): void { | |
// Create an alarm slayer and attach to the observable | |
this.alarmSlayer$ = new Subject<void>(); | |
warnExpire$.pipe(takeUntil(this.alarmSlayer$)); | |
} | |
private slayPreviousAlarm(): void { | |
// Halt by triggering the alarm slayer | |
if (this.alarmSlayer$) { | |
this.alarmSlayer$.next(); | |
this.alarmSlayer$.complete(); | |
} | |
} | |
private getMsecsBeforeExpire(): number { | |
return this.sessionExpireEpoch - Date.now(); | |
} | |
private getNextWarnEventDelay(msecsBeforeExpire: number): number { | |
return msecsBeforeExpire % this.ONE_SECOND; // this is the milliseconds portion of the time difference | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment