import { Observable , Subject } from 'rxjs' ; import { Readable } from 'stream' ; import { TaskWrapper } from './task-wrapper' ; import { ListrEventType } from '@constants/event.constants' ; import { ListrTaskState } from '@constants/state.constants' ; import { ListrErrorTypes , PromptError } from '@interfaces/listr-error.interface' ; import { ListrEvent , ListrOptions , ListrTask , ListrTaskResult } from '@interfaces/listr.interface' ; import { ListrGetRendererOptions , ListrGetRendererTaskOptions , ListrRendererFactory } from '@interfaces/renderer.interface' ; import { Listr } from '@root/listr' ; import { assertFunctionOrSelf } from '@utils/assert' ; import { PromptInstance } from '@utils/prompt.interface' ; import { getRenderer } from '@utils/renderer' ; import { generateUUID } from '@utils/uuid' ; /**
 * Create a task from the given set of variables and make it runnable.
 */ export class Task < Ctx , Renderer extends ListrRendererFactory > extends Subject < ListrEvent > { /** Unique id per task, randomly generated in the uuid v4 format */ public id : string ; /** The current state of the task. */ public state : string ; /** The task object itself, to further utilize it. */ public task : ( ctx : Ctx , task : TaskWrapper < Ctx , Renderer > ) => void | ListrTaskResult < Ctx > ; /** Extend current task with multiple subtasks. */ public subtasks : Task < Ctx , any > [ ] ; /** Title of the task */ public title ? : string ; /** Untouched unchanged title of the task */ public initialTitle ? : string ; /** Output data from the task. */ public output ? : string ; /** Skip current task. */ public skip : boolean | string | ( ( ctx : Ctx ) => boolean | string | Promise < boolean | string > ) ; /** Current retry number of the task if retrying */ public retry ? : { count : number ; withError ? : any ; } ; /**
     * A channel for messages.
     *
     * This requires a separate channel for messages like error, skip or runtime messages to further utilize in the renderers.
     */ public message : { /** Run time of the task, if it has been successfully resolved. */ duration ? : number ; /** Error message of the task, if it has been failed. */ error ? : string ; /** Skip message of the task, if it has been skipped. */ skip ? : string ; /** Rollback message of the task, if the rollback finishes */ rollback ? : string ; /** Retry messages */ retry ? : { count : number ; withError ? : any ; } ; } = { } ; /** Per task options for the current renderer of the task. */ public rendererTaskOptions : ListrGetRendererTaskOptions < Renderer > ; /** This will be triggered each time a new render should happen. */ public renderHook$ : Subject < void > ; public prompt : undefined | PromptInstance | PromptError ; private enabled : boolean ; private enabledFn : ListrTask < Ctx , Renderer > [ 'enabled' ] ; constructor ( public listr , public tasks , public options , public rendererOptions ) { super ( ) ; this . id = generateUUID ( ) ; this . title = this . tasks ?. title ; this . initialTitle = this . tasks ?. title ; this . task = this . tasks . task ; this . skip = this . tasks ?. skip ?? false ; this . enabledFn = this . tasks ?. enabled ?? true ; this . rendererTaskOptions = this . tasks . options ; this . renderHook$ = this . listr . renderHook$ ; this . subscribe ( ( ) => { this . renderHook$ . next ( ) ; } ) ; } set state$ ( state ) { this . state = state ; this . next ( { type : ListrEventType . STATE , data : state } ) ; if ( this . hasSubtasks ( ) && this . hasFailed ( ) ) { for ( const subtask of this . subtasks as Task < any , any > [ ] ) { if ( subtask . state === ListrTaskState . PENDING ) { subtask . state$ = ListrTaskState . FAILED ; } } } } set output$ ( data ) { this . output = data ; this . next ( { type : ListrEventType . DATA , data } ) ; } set message$ ( data ) { this . message = { ... this . message , ... data } ; this . next ( { type : ListrEventType . MESSAGE , data } ) ; } set title$ ( title ) { this . title = title ; this . next ( { type : ListrEventType . TITLE , data : title } ) ; } /**
     * A function to check whether this task should run at all via enable.
     */ async check ( ctx ) { if ( this . state === undefined ) { this . enabled = await assertFunctionOrSelf ( this . enabledFn , ctx ) ; this . next ( { type : ListrEventType . ENABLED , data : this . enabled } ) ; } } /** Returns whether this task has subtasks. */ public hasSubtasks ( ) { return this . subtasks ?. length > 0 ; } /** Returns whether this task is in progress. */ public isPending ( ) { return this . state === ListrTaskState . PENDING ; } /** Returns whether this task is skipped. */ public isSkipped ( ) { return this . state === ListrTaskState . SKIPPED ; } /** Returns whether this task has been completed. */ public isCompleted ( ) { return this . state === ListrTaskState . COMPLETED ; } /** Returns whether this task has been failed. */ public hasFailed ( ) { return this . state === ListrTaskState . FAILED ; } /** Returns whether this task has an active rollback task going on. */ public isRollingBack ( ) { return this . state === ListrTaskState . ROLLING_BACK ; } /** Returns whether the rollback action was successful. */ public hasRolledBack ( ) { return this . state === ListrTaskState . ROLLED_BACK ; } /** Returns whether this task has an actively retrying task going on. */ public isRetrying ( ) { return this . state === ListrTaskState . RETRY ; } /** Returns whether enabled function resolves to true. */ public isEnabled ( ) { return this . enabled ; } /** Returns whether this task actually has a title. */ public hasTitle ( ) { return typeof this ?. title === 'string' ; } /** Returns whether this task has a prompt inside. */ public isPrompt ( ) { return ! ! this . prompt ; } /** Run the current task. */ async run ( context , wrapper ) { const handleResult = ( result ) : Promise < any > => { if ( result instanceof Listr ) { result . options = { ... this . options , ... result . options } ; result . rendererClass = getRenderer ( 'silent' ) . renderer ; result . renderHook$ . subscribe ( ( ) : void => { this . renderHook$ . next ( ) ; } ) ; this . subtasks = result . tasks ; result . err = this . listr . err ; this . next ( { type : ListrEventType . SUBTASK } ) ; result = result . run ( context ) ; } else if ( this . isPrompt ( ) ) { } else if ( result instanceof Promise ) { result = result . then ( handleResult ) ; } else if ( result instanceof Readable ) { result = new Promise ( ( resolve , reject ) => { result . on ( 'data' , ( data ) => { this . output$ = data . toString ( ) ; } ) ; result . on ( 'error' , ( error ) => reject ( error ) ) ; result . on ( 'end' , ( ) => resolve ( null ) ) ; } ) ; } else if ( result instanceof Observable ) { result = new Promise ( ( resolve , reject ) => { result . subscribe ( { next : ( data ) => { this . output$ = data ; } , error : reject , complete : resolve } ) ; } ) ; } return result ; } ; const startTime = Date . now ( ) ; this . state$ = ListrTaskState . PENDING ; const skipped = await assertFunctionOrSelf ( this . skip , context ) ; if ( skipped ) { if ( typeof skipped === 'string' ) { this . message$ = { skip : skipped } ; } else if ( this . hasTitle ( ) ) { this . message$ = { skip : this . title } ; } else { this . message$ = { skip : 'Skipped task without a title.' } ; } this . state$ = ListrTaskState . SKIPPED ; return ; } try { const retryCount = this . tasks ?. retry && this . tasks ?. retry > 0 ? this . tasks . retry + 1 : 1 ; for ( let retries = 1 ; retries <= retryCount ; retries ++ ) { try { await handleResult ( this . task ( context , wrapper ) ) ; break ; } catch ( err ) { if ( retries !== retryCount ) { this . retry = { count : retries , withError : err } ; this . message$ = { retry : this . retry } ; this . title$ = this . initialTitle ; this . output = undefined ; wrapper . report ( err , ListrErrorTypes . WILL_RETRY ) ; this . state$ = ListrTaskState . RETRY ; } else { throw err ; } } } if ( this . isPending ( ) || this . isRetrying ( ) ) { this . message$ = { duration : Date . now ( ) - startTime } ; this . state$ = ListrTaskState . COMPLETED ; } } catch ( error ) { if ( this . prompt instanceof PromptError ) { error = new Error ( this . prompt . message ) ; } if ( this . tasks ?. rollback ) { wrapper . report ( error , ListrErrorTypes . WILL_ROLLBACK ) ; try { this . state$ = ListrTaskState . ROLLING_BACK ; await this . tasks . rollback ( context , wrapper ) ; this . state$ = ListrTaskState . ROLLED_BACK ; this . message$ = { rollback : this . title } ; } catch ( err ) { this . state$ = ListrTaskState . FAILED ; wrapper . report ( err , ListrErrorTypes . HAS_FAILED_TO_ROLLBACK ) ; throw err ; } if ( this . listr . options ?. exitAfterRollback !== false ) { throw new Error ( this . title ) ; } } else { this . state$ = ListrTaskState . FAILED ; if ( this . listr . options . exitOnError !== false && await assertFunctionOrSelf ( this . tasks ?. exitOnError , context ) !== false ) { wrapper . report ( error , ListrErrorTypes . HAS_FAILED ) ; throw error ; } else if ( ! this . hasSubtasks ( ) ) { wrapper . report ( error , ListrErrorTypes . HAS_FAILED_WITHOUT_ERROR ) ; } } } finally { this . complete ( ) ; } } }