import { isNull , isNumber , isString } from "ts-type-guards" ; import { Condition } from "./environment" ; type ActionResult = string | void ; type OperationResult = OperationFailure | undefined ; const SUCCESS = undefined ; export const enum Reason { DEPENDENCIES , INTERNAL } export type OperationFailure = Readonly < { reason : Reason . DEPENDENCIES ; dependencies : ReadonlyArray < { key : string ; selector : string ; } > ; } | { reason : Reason . INTERNAL ; message : string ; } > ; export type OperationAndFailure < K extends string > = Readonly < { operation : Operation < K > ; result : OperationFailure ; } > ; type BaseOperation = Readonly < { condition : Condition ; description : string ; deferUntil ? : ( state : DocumentReadyState ) => boolean ; } > ; type DependentOperationSpec < K extends string > = BaseOperation & Readonly < { dependencies : { [ k in K ] : string ; } ; action : ( e : { [ k in K ] : HTMLElement ; } ) => ActionResult ; } > ; type IndependentOperationSpec = BaseOperation & Readonly < { dependencies ? : undefined ; action : ( ) => ActionResult ; } > ; export type Operation < K extends string > = DependentOperationSpec < K > | IndependentOperationSpec ; export function operation < K extends string > ( spec ) { return spec as Operation < any > ; } export type FailuresHandler = ( failures : ReadonlyArray < OperationAndFailure < any > > ) => void ; export type Plan = Readonly < { operations : ReadonlyArray < Operation < any > > ; interval : number ; tryUntil : ( state : DocumentReadyState ) => boolean ; extraTries : number ; handleFailures : FailuresHandler ; } > ; export function run ( plan ) { function recurse ( operations , failures , triesLeft ? ) { const lastTry = isNumber ( triesLeft ) && triesLeft <= 0 ; const operationsToRunNow = [ ] ; const remaining = [ ] ; const readyState = document . readyState ; for ( const o of operations ) { const shouldRunNow = o . deferUntil === undefined || o . deferUntil ( readyState ) ; ( shouldRunNow ? operationsToRunNow : remaining ) . push ( o ) ; } for ( const o of operationsToRunNow ) { const result = tryToPerform ( o ) ; if ( result !== SUCCESS ) { switch ( result . reason ) { case Reason . DEPENDENCIES : if ( lastTry ) { failures . push ( { result , operation : o } ) ; } else { remaining . push ( o ) ; } break ; case Reason . INTERNAL : failures . push ( { result , operation : o } ) ; break ; } } } if ( remaining . length > 0 ) { setTimeout ( ( ) => recurse ( remaining , failures , ( isNumber ( triesLeft ) ? triesLeft - 1 : plan . tryUntil ( readyState ) ? plan . extraTries : undefined ) ) , plan . interval ) ; } else if ( failures . length > 0 ) { plan . handleFailures ( failures ) ; } } recurse ( plan . operations . filter ( o => o . condition ( window ) ) , [ ] ) ; } function tryToPerform < K extends string > ( o ) { const dependencies = o . dependencies === undefined ? { } as { [ k in K ] : string ; } : o . dependencies ; const queryResults = Object . entries < string > ( dependencies ) . map ( ( [ key , selector ] ) => ( { key , selector , element : document . querySelector < HTMLElement > ( selector ) , } ) ) ; const missingDependencies = queryResults . filter ( x => isNull ( x . element ) ) ; if ( missingDependencies . length > 0 ) { return { reason : Reason . DEPENDENCIES , dependencies : missingDependencies } ; } const e = queryResults . reduce ( ( acc , x ) => Object . defineProperty ( acc , x . key , { value : x . element } ) , { } as { [ k in K ] : HTMLElement ; } ) ; return fromActionResult ( o . action ( e ) ) ; } function fromActionResult ( r ) { return isString ( r ) ? { reason : Reason . INTERNAL , message : r } : SUCCESS ; }