Newer
Older
PixelPaintWar / apps / client / src / scenes / game / application / time / ClockSyncLoop.ts
/**
 * ClockSyncLoop
 * 時刻同期PING送信の定期実行を管理する
 * 可変間隔スケジュールと停止処理を一元化する
 */
import { SYSTEM_TIME_PROVIDER } from "@client/scenes/game/application/time/TimeProvider";

/** 時刻同期ループ初期化入力型 */
export type ClockSyncLoopOptions = {
  sendPing: (clientTime: number) => void;
  getNextIntervalMs: () => number;
  nowMsProvider?: () => number;
};

/** 時刻同期PING送信の開始停止を管理する */
export class ClockSyncLoop {
  private readonly sendPing: (clientTime: number) => void;
  private readonly getNextIntervalMs: () => number;
  private readonly nowMsProvider: () => number;
  private timeoutId: ReturnType<typeof setTimeout> | null = null;

  constructor({
    sendPing,
    getNextIntervalMs,
    nowMsProvider = SYSTEM_TIME_PROVIDER.now,
  }: ClockSyncLoopOptions) {
    this.sendPing = sendPing;
    this.getNextIntervalMs = getNextIntervalMs;
    this.nowMsProvider = nowMsProvider;
  }

  /** 同期ループを開始する */
  public start(): void {
    this.stop();
    this.executeOnce();
  }

  /** 同期ループを停止する */
  public stop(): void {
    if (this.timeoutId === null) {
      return;
    }

    clearTimeout(this.timeoutId);
    this.timeoutId = null;
  }

  /** 同期ループを停止して破棄する */
  public dispose(): void {
    this.stop();
  }

  /** 現在ループが動作中かを返す */
  public isRunning(): boolean {
    return this.timeoutId !== null;
  }

  private executeOnce(): void {
    this.sendPing(this.nowMsProvider());

    const intervalMs = this.getNextIntervalMs();
    this.timeoutId = setTimeout(() => {
      this.executeOnce();
    }, intervalMs);
  }
}