import ld from "lodash" ; import { CustomError } from "ts-custom-error" ; import { InternalError } from "./internal_error" ; import { MessageLogger , MessageStreamer , MessageType , TaskState , TaskStatus , } from "./message/index" ; import { immediatePromise } from "./sleep" ; const debugTaskTime = false ; export interface TaskDefinitions { [ name ] : string ; } export type TaskObserversKnown < Names extends string > = { [ N in Names ] : TaskObserver ; } ; export interface TaskObserversUnknown { [ name ] : TaskObserver | undefined ; } export type TaskObservers < Names extends string > = TaskObserversKnown < Names > & TaskObserversUnknown ; export interface TaskAddOptions { createOnly ? : boolean ; trivial ? : boolean ; } export interface TaskGroup { add < T extends TaskDefinitions > ( tasks , options ? ) : TaskObservers < Extract < keyof T , string > > ; task ( name ) : TaskObserver ; } export interface TaskGroupOptions { serial ? : boolean ; } const defaultTaskGroupOptions = { serial : true , } ; export interface TaskObserver { description : string ; readonly name : string ; readonly logger : MessageLogger ; readonly options : TaskObserverOptions ; readonly state : TaskState ; updateStatus ( txt ) : void ; childGroup ( options ? ) : TaskGroup ; started ( ) : void ; skipped ( ) : void ; complete ( ) : void ; complete < T > ( p ) : Promise < T > ; failed ( err ) : void ; } export interface TaskObserverOptions { description ? : string ; logger ? : MessageLogger ; trivial ? : boolean ; } const defaultTaskObserverOptions = { trivial : false , } ; export interface TaskInfo { description : string ; trivial : boolean ; } export function parseTaskInfo ( jsonString ) { try { const ti = JSON . parse ( jsonString ) ; if ( typeof ti . description !== "string" ) throw new InternalError ( `TaskInfo description invalid` ) ; if ( typeof ti . trivial !== "boolean" ) throw new InternalError ( `TaskInfo trivial invalid` ) ; return ti ; } catch ( err ) { throw new InternalError ( `Unable to parse TaskInfo from task message: ${ err . message }` ) ; } } export function taskInfoToString ( ti ) { return JSON . stringify ( ti ) ; } export function createTaskObserver ( name , options = { } ) { return new TaskObserverImpl ( name , options ) ; } class TaskObserverImpl implements TaskObserver { readonly logger : MessageLogger ; readonly options : Required < TaskObserverOptions > ; startTime ? : number ; private description_ : string ; private state_ = TaskState . Created ; private childGroup_ ? : TaskGroupImpl ; constructor ( readonly name , options ) { const { logger , description , trivial } = { ... defaultTaskObserverOptions , description : name , logger : new MessageStreamer ( name ) , ... options } ; this . logger = logger ; this . description_ = description ; this . options = { logger , description , trivial , } ; this . log ( this . state_ , taskInfoToString ( { description , trivial } ) ) ; } get description ( ) { return this . description_ ; } set description ( newDesc ) { this . log ( TaskStatus . Description , newDesc ) ; this . description_ = newDesc ; } get state ( ) { return this . state_ ; } updateStatus ( txt ) { this . log ( TaskStatus . Status , txt ) ; } childGroup ( options = { } ) { if ( ! this . childGroup_ ) { const tgOpts = { ... defaultTaskGroupOptions , ... options } ; this . childGroup_ = new TaskGroupImpl ( tgOpts , this . options ) ; this . log ( TaskStatus . ChildGroup , JSON . stringify ( tgOpts ) ) ; } return this . childGroup_ ; } started ( ) { this . updateState ( TaskState . Started ) ; } skipped ( ) { this . updateState ( TaskState . Skipped ) ; } complete ( ) ; complete < T > ( p ) ; complete < T > ( p ? ) { const done = ( val ? ) => { this . updateState ( TaskState . Complete ) ; return immediatePromise ( ) . then ( ( ) => val as T ) ; } ; const fail = ( err ) => { this . failed ( err ) ; throw err ; } ; if ( ld . isFunction ( p ) ) { this . started ( ) ; return immediatePromise ( ) . then ( p ) . then ( done ) . catch ( fail ) ; } if ( p !== undefined ) { return p . then ( done ) . catch ( fail ) ; } this . updateState ( TaskState . Complete ) ; } failed ( err ) { const msg = ld . isError ( err ) ? err . message : err . toString ( ) ; this . updateState ( TaskState . Failed , msg ) ; } private checkTransition ( current , next ) { if ( next === current ) { throw new TaskObserverStateError ( this , current , next ) ; } let errored = false ; switch ( current ) { case TaskState . Complete : case TaskState . Failed : case TaskState . Skipped : errored = true ; break ; case TaskState . Started : if ( next === TaskState . Created ) errored = true ; break ; case TaskState . Created : if ( next === TaskState . Complete ) errored = true ; break ; default : return badState ( this , current ) ; } if ( errored ) throw new TaskObserverStateError ( this , current , next ) ; } private log ( event , txt ? ) { let msg = `[${ event }]` ; if ( txt !== undefined ) msg += `: ${ txt }` ; this . logger . log ( MessageType . task , msg ) ; } private updateState ( state , msg ? ) { switch ( state ) { case TaskState . Created : case TaskState . Started : case TaskState . Complete : case TaskState . Failed : case TaskState . Skipped : break ; default : return badState ( this , state ) ; } this . checkTransition ( this . state_ , state ) ; if ( debugTaskTime ) { if ( state === TaskState . Started ) this . startTime = Date . now ( ) ; if ( state === TaskState . Complete && this . startTime ) { msg = `${ Date . now ( ) - this . startTime }ms` ; } } this . state_ = state ; this . log ( state , msg ) ; } } export class TaskObserverError extends CustomError { constructor ( task , msg ) { super ( `TaskObserver [${ task . name }]: ${ msg }` ) ; } } export class TaskObserverStateError extends TaskObserverError { constructor ( task , current , next ) { super ( task , `invalid state transition (${ current } -> ${ next })` ) ; } } function badState ( task , x ) { throw new TaskObserverError ( task , `Invalid state ${ x }` ) ; } const defaultTaskAddOptions = { createOnly : true , trivial : false , } ; class TaskGroupImpl implements TaskGroup { private tasks_ : TaskObserversUnknown = Object . create ( null ) ; constructor ( readonly options , readonly taskOptions ) { } add < T extends TaskDefinitions > ( tasks , options = { } ) { const { createOnly , trivial } = { ... defaultTaskAddOptions , ... options } ; if ( createOnly ) { for ( const name of Object . keys ( tasks ) ) { if ( this . tasks_ [ name ] ) { throw new Error ( `A task with name ${ name } already exists` ) ; } } } const parentLogger = this . taskOptions . logger ; for ( const name of Object . keys ( tasks ) ) { if ( this . tasks_ [ name ] ) continue ; const logger = parentLogger && parentLogger . createChild ( name ) ; const task = new TaskObserverImpl ( name , { ... this . taskOptions , logger , description : tasks [ name ] , trivial , } ) ; this . tasks_ [ name ] = task ; } return this . tasks_ as TaskObservers < Extract < keyof T , string > > ; } task ( name ) { const obs = this . tasks_ [ name ] ; if ( ! obs ) throw new Error ( `Task name ${ name } not found` ) ; return obs ; } }