import { assert } from "./assert" ; import { Deferred } from "./promises" ; export interface ITimer { /**
     * True if timer is currently running
     */ readonly hasTimer : boolean ; /**
     * Starts the timer
     */ start ( ) : void ; /**
     * Cancels the timer if already running
     */ clear ( ) : void ; } interface ITimeout { /**
     * Tick that timeout was started.
     */ startTick : number ; /**
     * Timeout duration in ms.
     */ duration : number ; /**
     * Handler to execute when timeout ends.
     */ handler : ( ) => void ; } interface IRunningTimerState extends ITimeout { /**
     * JavaScript Timeout object.
     */ timeout : ReturnType < typeof setTimeout > ; /**
     * Intended duration in ms.
     */ intendedDuration : number ; /**
     * Intended restart timeout.
     */ restart ? : ITimeout ; } /**
 * This class is a thin wrapper over setTimeout and clearTimeout which
 * makes it simpler to keep track of recurring timeouts with the same
 * or similar handlers and timeouts.
 */ export class Timer implements ITimer { /**
     * Returns true if the timer is running.
     */ public get hasTimer ( ) { return ! ! this . runningState ; } private runningState : IRunningTimerState | undefined ; constructor ( private readonly defaultTimeout , private readonly defaultHandler , private readonly getCurrentTick = ( ) => Date . now ( ) ) { } param ms param handler public start ( ms = this . defaultTimeout , handler = this . defaultHandler ) { this . startCore ( ms , handler , ms ) ; } /**
     * Calls clearTimeout on the underlying timeout if running.
     */ public clear ( ) { if ( ! this . runningState ) { return ; } clearTimeout ( this . runningState . timeout ) ; this . runningState = undefined ; } param ms param handler public restart ( ms ? , handler ? ) { if ( ! this . runningState ) { this . start ( ms , handler ) ; } else { const duration = ms ?? this . runningState . intendedDuration ; const handlerToUse = handler ?? this . runningState . restart ?. handler ?? this . runningState . handler ; const remainingTime = this . calculateRemainingTime ( this . runningState ) ; if ( duration < remainingTime ) { this . start ( duration , handlerToUse ) ; } else if ( duration === remainingTime ) { this . runningState . handler = handlerToUse ; this . runningState . restart = undefined ; this . runningState . intendedDuration = duration ; } else { this . runningState . restart = { startTick : this . getCurrentTick ( ) , duration , handler : handlerToUse , } ; } } } private startCore ( duration , handler , intendedDuration ) { this . clear ( ) ; this . runningState = { startTick : this . getCurrentTick ( ) , duration , intendedDuration , handler , timeout : setTimeout ( ( ) => this . handler ( ) , duration ) , } ; } private handler ( ) { assert ( ! ! this . runningState , 0x00a ) ; const restart = this . runningState . restart ; if ( restart !== undefined ) { const remainingTime = this . calculateRemainingTime ( restart ) ; this . startCore ( remainingTime , ( ) => restart . handler ( ) , restart . duration ) ; } else { const handler = this . runningState . handler ; this . clear ( ) ; handler ( ) ; } } private calculateRemainingTime ( runningTimeout ) { const elapsedTime = this . getCurrentTick ( ) - runningTimeout . startTick ; return runningTimeout . duration - elapsedTime ; } } export interface IPromiseTimerResult { timerResult : "timeout" | "cancel" ; } /**
 * Timer which offers a promise that fulfills when the timer
 * completes.
 */ export interface IPromiseTimer extends ITimer { /**
     * Starts the timer and returns a promise that
     * resolves when the timer times out or is canceled.
     */ start ( ) : Promise < IPromiseTimerResult > ; } /**
 * This class is a wrapper over setTimeout and clearTimeout which
 * makes it simpler to keep track of recurring timeouts with the
 * same handlers and timeouts, while also providing a promise that
 * resolves when it times out.
 */ export class PromiseTimer implements IPromiseTimer { private deferred ? : Deferred < IPromiseTimerResult > ; private readonly timer : Timer ; public get hasTimer ( ) { return this . timer . hasTimer ; } constructor ( defaultTimeout , defaultHandler ) { this . timer = new Timer ( defaultTimeout , ( ) => this . wrapHandler ( defaultHandler ) ) ; } public async start ( ms ? , handler ? ) { this . clear ( ) ; this . deferred = new Deferred < IPromiseTimerResult > ( ) ; this . timer . start ( ms , handler ? ( ) => this . wrapHandler ( handler ) : undefined ) ; return this . deferred . promise ; } public clear ( ) { this . timer . clear ( ) ; if ( this . deferred ) { this . deferred . resolve ( { timerResult : "cancel" } ) ; this . deferred = undefined ; } } protected wrapHandler ( handler ) { handler ( ) ; assert ( ! ! this . deferred , 0x00b ) ; this . deferred . resolve ( { timerResult : "timeout" } ) ; this . deferred = undefined ; } }