Source

lib/events/Scheduler.ts

import { SessionCheckResponse } from "../Dto";

// Type representing data returned by the session check callback.
export type SessionCheckResult =
  | (Omit<SessionCheckResponse, "expiration_time"> & {
      expiration: number;
    })
  | null;

/**
 * Callback type for performing a session check.
 * @ignore
 */
type SessionCheckCallback = () => Promise<SessionCheckResult>;

/**
 * Callback type for handling session timeout events.
 * @ignore
 */
type SessionExpiredCallback = () => void;

/**
 * Manages scheduling for periodic and timeout-based session checks.
 *
 * @category SDK
 * @subcategory Internal
 * @param {number} checkInterval - The interval in milliseconds between periodic session checks.
 * @param {SessionCheckCallback} checkSession - The callback function to perform a session check.
 * @param {SessionExpiredCallback} onSessionExpired - The callback function to handle session timeout events.
 */
export class Scheduler {
  private intervalID: ReturnType<typeof setInterval> | null = null; // Identifier for the periodic check interval.
  private timeoutID: ReturnType<typeof setTimeout> | null = null; // Identifier for the session expiration timeout.
  private readonly checkInterval: number; // The interval between periodic session checks.
  private readonly checkSession: SessionCheckCallback; // The callback function to perform a session check.
  private readonly onSessionExpired: SessionExpiredCallback; // The callback function to handle session expired events.

  // eslint-disable-next-line require-jsdoc
  constructor(
    checkInterval: number,
    checkSession: SessionCheckCallback,
    onSessionExpired: SessionExpiredCallback,
  ) {
    this.checkInterval = checkInterval;
    this.checkSession = checkSession;
    this.onSessionExpired = onSessionExpired;
  }

  /**
   * Handles the session expiration when it is about to expire soon.
   * Stops any ongoing checks and schedules a timeout for the expiration.
   *
   * @param {number} timeToExpiration - The time in milliseconds until the session expires.
   */
  scheduleSessionExpiry(timeToExpiration: number): void {
    this.stop();
    this.timeoutID = setTimeout(async () => {
      this.stop();
      this.onSessionExpired();
    }, timeToExpiration);
  }

  /**
   * Starts the session check process.
   * Determines when the next check should run based on the last known check time and session expiration.
   * If the session is expiring soon, schedules an expiration event instead of starting periodic checks.
   *
   * @param {number} lastCheck - The timestamp (in milliseconds) of the last session check.
   * @param {number} expiration - The timestamp (in milliseconds) of when the session expires.
   */
  start(lastCheck: number = 0, expiration: number = 0): void {
    const timeToNextCheck = this.calcTimeToNextCheck(lastCheck);

    if (this.sessionExpiresSoon(expiration)) {
      this.scheduleSessionExpiry(timeToNextCheck);
      return;
    }

    // Schedule the first check after an optional delay
    this.timeoutID = setTimeout(async () => {
      let result = await this.checkSession();

      if (result.is_valid) {
        if (this.sessionExpiresSoon(result.expiration)) {
          this.scheduleSessionExpiry(result.expiration - Date.now());
          return;
        }

        // Begin periodic checks
        this.intervalID = setInterval(async () => {
          result = await this.checkSession();

          if (result.is_valid) {
            if (this.sessionExpiresSoon(result.expiration)) {
              this.scheduleSessionExpiry(result.expiration - Date.now());
            }
          } else {
            this.stop();
          }
        }, this.checkInterval);
      } else {
        this.stop();
      }
    }, timeToNextCheck);
  }

  /**
   * Stops the session check process and clears all timers.
   */
  stop(): void {
    if (this.timeoutID) {
      clearTimeout(this.timeoutID);
      this.timeoutID = null;
    }

    if (this.intervalID) {
      clearInterval(this.intervalID);
      this.intervalID = null;
    }
  }

  /**
   * Checks if the scheduler is currently running.
   * @returns {boolean} True if the scheduler is running; otherwise, false.
   */
  isRunning(): boolean {
    return this.timeoutID !== null || this.intervalID !== null;
  }
  /**
   * Checks if the session is about to expire.
   * @param {number} expiration - Timestamp when the session will expire.
   * @returns {boolean} True if the session is about to expire; otherwise, false.
   */
  sessionExpiresSoon(expiration: number): boolean {
    return expiration > 0 && expiration - Date.now() <= this.checkInterval;
  }

  /**
   * Calculates the time until the next session check should occur.
   *
   * @param {number} lastCheck - The timestamp (in milliseconds) of the last session check.
   * @returns {number} The time in milliseconds until the next check should be performed.
   */
  calcTimeToNextCheck(lastCheck: number): number {
    const timeSinceLastCheck = Date.now() - lastCheck;
    return this.checkInterval >= timeSinceLastCheck
      ? this.checkInterval - (timeSinceLastCheck % this.checkInterval)
      : 0;
  }
}