Skip to content

Instantly share code, notes, and snippets.

@tw3
Created June 30, 2020 14:07
Show Gist options
  • Save tw3/52822f726a98c4397073bfb0765d5882 to your computer and use it in GitHub Desktop.
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
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