lib/Classes/Public/TMBroker.ps1

class TMBroker {

    #region Non-Static Properties

    [TMBrokerSetting]$Settings
    [TMTask]$Task
    [TMBrokerSubject]$Init
    [TMSession]$TMSession
    [TMBrokerEventData]$EventData
    [TMBrokerStatus]$Status
    [Object]$Cache
    [System.Collections.Generic.List[System.Object]]$Subjects

    #endregion Non-Static Properties


    #region Constructors

    # Initializes an empty TMBroker object with default settings
    TMBroker() {
        $this.Settings = [TMBrokerSetting]::new()
        $this.Status = [TMBrokerStatus]::new()
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker([String]$_type, [String]$_taskProperty, [String[]]$_matchingCriteria) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskProperty, $_matchingCriteria)
        $this.Status = [TMBrokerStatus]::new()
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker([String]$_type, [ScriptBlock]$_matchExpression) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_matchExpression)
        $this.Status = [TMBrokerStatus]::new()
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker([String]$_type, [TMBrokerTaskFilter]$_taskFilter) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskFilter)
        $this.Status = [TMBrokerStatus]::new()
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [String]$_taskProperty,
        [String[]]$_matchingCriteria,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskProperty, $_matchingCriteria, $_timeout, $_pauseSeconds)
        $this.Status = [TMBrokerStatus]::new($_timeout)
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [ScriptBlock]$_matchExpression,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_matchExpression, $_timeout, $_pauseSeconds)
        $this.Status = [TMBrokerStatus]::new($_timeout)
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [TMBrokerTaskFilter]$_taskFilter,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskFilter, $_timeout, $_pauseSeconds)
        $this.Status = [TMBrokerStatus]::new($_timeout)
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [String]$_taskProperty,
        [String[]]$_matchingCriteria,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskProperty, $_matchingCriteria, $_timeout, $_pauseSeconds, $_parallel, $_throttle)
        $this.Status = [TMBrokerStatus]::new($_timeout, $_throttle)
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [ScriptBlock]$_matchExpression,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_matchExpression, $_timeout, $_pauseSeconds, $_parallel, $_throttle)
        $this.Status = [TMBrokerStatus]::new($_timeout, $_throttle)
        $this.EventData = [TMBrokerEventData]::new()
    }

    TMBroker(
        [String]$_type,
        [TMBrokerTaskFilter]$_taskFilter,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.Settings = [TMBrokerSetting]::new($_type, $_taskFilter, $_timeout, $_pauseSeconds, $_parallel, $_throttle)
        $this.Status = [TMBrokerStatus]::new($_timeout, $_throttle)
        $this.EventData = [TMBrokerEventData]::new()
    }

    #endregion Constructors


    #region Non-Static Methods

    <#
        Method: GetEventData
        Description: Loads the EventData property using this object's TMSession
        Parameters: None
    #>

    [void]GetEventData() {
        if (-not $this.TMSession) {
            throw 'A TM Session is required to invoke this method'
        }

        if ($this.Settings.SubjectScope.FilterType -eq 'TaskFilter') {
            $this.EventData.GetEventData(
                $this.TMSession.UserContext.project.id,
                $this.TMSession.UserContext.event.name,
                $this.TMSession.Name,
                $this.Settings.SubjectScope.TaskFilter
            )
        } else {
            $this.EventData.GetEventData(
                $this.TMSession.UserContext.project.id,
                $this.TMSession.UserContext.event.name,
                $this.TMSession.Name
            )
        }
    }


    <#
        Method: GetTaskData
        Description: Loads all of the broker-related tasks
        Parameters:
            TaskId - The Id of broker task
    #>

    [void]GetTaskData($TaskId) {

        if (-not $this.EventData) {
            throw 'Event data must be loaded before invoking this method'
        }

        # Store this Broker Task's data
        $this.Task = ($this.EventData.Tasks | Where-Object { $_.Id -eq $TaskId })

        if (-not $this.Task) {
            $this.Task = Get-TMTask -Id $TaskId -TMSession $this.TMSession.Name
        }

        # Determine if there is an init cache Task
        $this.GetInitTask()

        # Get all of the subject tasks
        $this.GetSubjectTasks()
    }


    <#
        Method: GetInitTask
        Description: Gets the init task, if present, from the Event's task data
        Parameters: None
    #>

    [void]GetInitTask() {
        if ($this.Settings.SubjectScope.Type -eq 'Inline') {
            if (-not $this.Task) {
                throw 'Broker Task data must be loaded before invoking this method'
            }
            $InitTask = (
                $this.EventData.Tasks |
                    Where-Object { $_.Id -in $this.Task.Successors.TaskId } |
                        Where-Object -FilterScript $this.Settings.SubjectScope.MatchExpression
            )
            if ($InitTask) {
                $this.Init = [TMBrokerSubject]::new($InitTask)
            }
        }
    }


    <#
        Method: GetSubjectTasks
        Description: Gets all of the subject tasks that will be managed by the broker from the Event's task data
        Parameters: None
    #>

    [void]GetSubjectTasks() {
        switch ($this.Settings.SubjectScope.Type) {
            'Inline' {
                if (-not $this.Task -and -not $this.Init) {
                    throw 'Broker Task or Init Task data must be loaded before invoking this method'
                }

                # Initialize the subjects list
                $this.Subjects = [System.Collections.Generic.List[System.Collections.Generic.List[TMBrokerSubject]]]::new()

                foreach ($TaskId in ($this.Init.Task.Successors.TaskId ?? $this.Task.Successors.TaskId)) {

                    # Initialize a list to hold all of the subject tasks for a specific asset
                    $Workflow = [System.Collections.Generic.List[TMBrokerSubject]]::new()

                    # Find the first/direct successor subject task
                    $SubjectTask = $this.EventData.Tasks | Where-Object { $_.Id -eq $TaskId }

                    $i = 0
                    while ($SubjectTask) {
                        $i++

                        # Add the subject task data to the workflow
                        $Workflow.Add([TMBrokerSubject]::new($SubjectTask, $i))

                        # Look for the next subject task in the workflow
                        $SubjectTask = $this.EventData.Tasks | Where-Object { $_.Id -eq $SubjectTask.Successors.TaskId }
                    }

                    # Record how many tasks are in each asset's workflow
                    $this.Status.WorkflowTaskCount = $i

                    # Add this workflow to the list of subjects
                    $this.Subjects.Add($Workflow)
                }
            }

            'Service' {
                # Initialize the subjects list
                $this.Subjects = [System.Collections.Generic.List[TMBrokerSubject]]::new()

                # Filter all Tasks down to the specified scope
                $ServiceSubjectTasks = $this.EventData.Tasks | Where-Object {
                    ($_.id -ne $Broker.task.id ) -and
                    ($_.Action.Id -ne 0) -and
                    ($_.Action.name -notlike '*broker*') -and
                    -not ($_.Action.MethodParams | Where-Object { $_.ParamName -match 'get_' })
                }

                # Apply the match expression to filter tasks further
                if ($this.Settings.SubjectScope.FilterType -eq 'MatchExpression') {
                    $ServiceSubjectTasks = $ServiceSubjectTasks | Where-Object -FilterScript $this.Settings.SubjectScope.MatchExpression
                }

                foreach ($Task in $ServiceSubjectTasks) {
                    $this.Subjects.Add([TMBrokerSubject]::new($Task))
                }
            }

            default { }
        }
    }


    <#
        Method: PopulateCache
        Description: Invokes the Init Task's Action to fill the cache
        Parameters: None
    #>

    [void]PopulateCache() {
        if (-not $this.Init) {
            throw 'Init Task data must be loaded before invoking this method'
        }

        $this.Init.Invoke($this.TMSession.Name)
    }


    <#
        Method: RefreshTaskStatuses
        Description: Updates each Task's status using fresh data from TM
        Parameters: None
    #>

    [void]RefreshTaskStatuses() {
        $TaskIds = [Array]@(
            $this.Subjects.Task.Id
            $this.Task.Id
            $this.Init.Task.Id
        ) | Where-Object { $_ -gt 0 }

        $Statement = "find Task by 'id' inList([$($TaskIds -join ', ')]) fetch 'id', 'status', 'lastUpdated'"
        $TaskStatuses = Invoke-TMQLStatement -TMSession $this.TMSession.Name -Statement $Statement

        $this.Task.Status = ($TaskStatuses | Where-Object Id -eq $this.Task.Id).Status
        if ($this.Init) {
            $this.Init.Task.Status = ($TaskStatuses | Where-Object Id -eq $this.Init.Id).Status
        }

        switch ($this.Settings.SubjectScope.Type) {
            'Inline' {
                foreach ($Workflow in $this.Subjects) {
                    foreach ($Subject in $Workflow) {
                        $Subject.Task.Status = ($TaskStatuses | Where-Object Id -eq $Subject.Task.Id).Status
                    }
                }
            }

            'Service' {
                foreach ($Subject in $this.Subjects) {
                    $TaskFromTM = $TaskStatuses | Where-Object Id -eq $Subject.Task.Id
                    $Subject.Task.Status = $TaskFromTM.Status
                    $Subject.Task.LastUpdated = $TaskFromTM.LastUpdated

                    ## Review Task states to update throttling settings
                    if ($this.Settings.Parallel) {

                        ## Handle updating Subject Task data based on the status of the task
                        switch ($Subject.Task.Status) {
                            'Started' {
                                ## Mark the Action as Started so the Broker ignores it for next time
                                $Subject.Action.ExecutionStatus = 'Started'
                            }

                            'Completed' {
                                ## Check to ensure the broker does not believe it's running completed Tasks
                                if ($this.Status.ActiveSubjects -contains $Subject.Task.Id) {
                                    $this.Status.ActiveSubjects.Remove($Subject.Task.Id)
                                }
                                $Subject.Action.ExecutionStatus = 'Successful'
                            }

                            'Hold' {
                                ## If the Task's status was changed either manually or due to failure,
                                ## reset the execution status so that it can be re-run
                                if ($this.Status.ActiveSubjects -contains $Subject.Task.Id) {
                                    $this.Status.ActiveSubjects.Remove($Subject.Task.Id)
                                }
                                $Subject.Action.ExecutionStatus = 'Pending'

                                # Check if a retry was defined in the Action params
                                if ($Subject.Action.Settings.RetryCount -gt 0) {
                                    if ($Subject.Task.LastUpdated) {

                                        # Check if enough time has elapsed
                                        $ElapsedTime = New-TimeSpan -Start $Subject.Task.LastUpdated -End (Get-Date)
                                        if ($ElapsedTime.TotalSeconds -ge $Subject.Action.Settings.WaitSeconds) {

                                            # Attempt to reset the Task Action and Reset the Task to Ready
                                            try {
                                                Write-Verbose "Retrying Task #: $($Subject.Task.Number). Retries left: $($Subject.Action.Settings.RetryCount)"
                                                Reset-TMTaskAction -TMSession $this.TMSession.Name -TaskId $Subject.Task.Id
                                                Set-TMTaskStatus -TMSession $this.TMSession.Name -TaskId $Subject.Task.Id -Status 'Ready'
                                                $Subject.Action.Settings.RetryCount--
                                            }
                                            catch {
                                                Write-Host "Could not retry Task # $($Subject.Task.Number): $($_.Exception.Message)" -ForegroundColor Magenta
                                            }
                                        }
                                    }
                                }
                            }

                            { $_ -in 'Pending', 'Ready' } {
                                ## If the Task's status was changed either manually or due to failure,
                                ## reset the execution status so that it can be re-run
                                if ($this.Status.ActiveSubjects -contains $Subject.Task.Id) {
                                    $this.Status.ActiveSubjects.Remove($Subject.Task.Id)
                                }
                                $Subject.Action.ExecutionStatus = 'Pending'
                            }
                        }
                    }
                }
            }
        }
    }


    <#
        Method: RefreshBrokerProgress
        Description: Updates the TMBrokerProgress properties on this Status object to be used for tracking and progress bars
        Parameters: None
    #>

    [void]RefreshBrokerProgress() {
        $this.Status.RemainingTasks.Value = ($this.Subjects | Where-Object { $_.Task.Status -ne 'Completed' -and $_.Action.ExecutionStatus -eq 'Pending' }).Count
        $this.Status.RemainingMinutes.Value = [Math]::Ceiling($this.Settings.Timing.TimeoutMinutes - $this.Settings.Timing.Timer.Elapsed.TotalMinutes)
        if ($this.Settings.Parallel) {
            $this.Status.Throttle.Value = $this.Status.ActiveSubjects.Count
        }
    }


    <#
        Method: Run
        Description: Executes the scoped subject Tasks
        Parameters: None
    #>

    [void]Run() {

        $this.Status.RemainingTasks.MaxValue = $this.Subjects.Count

        Write-Progress -Id 1 -ParentId 0 -Activity 'Subject Tasks' -Status "$($this.Subjects.Count) remaining tasks" -PercentComplete 0
        Write-Progress -Id 2 -ParentId 0 -Activity 'Timeout' -Status "$($this.Settings.Timing.TimeoutMinutes) minutes left" -PercentComplete 0
        if ($this.Settings.Parallel) {
            Write-Progress -Id 3 -ParentId 0 -Activity 'Throttle' -Status "$($this.Status.ActiveSubjects.Count) of $($this.Settings.Throttle)" -PercentComplete 0
        }

        $this.Settings.Timing.Timer.Start()
        $this.RefreshTaskStatuses()
        $this.RefreshBrokerProgress()

        while (
            ($this.Settings.Timing.Timer.Elapsed.TotalMinutes -lt $this.Settings.Timing.TimeoutMinutes) -and
            ($this.Subjects | Where-Object { $_.Action.ExecutionStatus -eq 'Pending' })
        ) {

            ## Saftey Check the broker task status in TM, exit if the task status is not Started
            if ($this.Task.Status -ne 'Started') {
                throw 'The status of the Broker Task has changed outside of TMConsole'
            }

            ## Force a refresh after a few tasks being executed
            if ($this.Status.TasksExecutedSinceRefresh -ge 3) {
                $this.RefreshTaskStatuses()
                $this.Status.TasksExecutedSinceRefresh = 0
            }

            switch ($this.Settings.SubjectScope.Type) {

                ## Inline Brokers run a workflow step worth of tasks at once
                'Inline' {
                    foreach ($Workflow in $this.Subjects) {
                        $Subject = $Workflow | Where-Object { $_.Task.Status -ne 'Completed' -and $_.Action.ExecutionStatus -eq 'Pending' } |
                            Sort-Object Order | Select-Object -First 1

                        if ($Subject) {
                            $Subject.Invoke($this.TMSession.Name, $this.Cache)
                            $this.Status.TasksExecutedSinceRefresh++
                        }
                    }
                }

                ## Service Brokers run one task at a time, when they become ready
                'Service' {

                    ## Get the most preferred actionable subject
                    $PreferredActionableSubject = $this.Subjects |
                        Where-Object { $_.Task.Status -eq 'Ready' -and $_.Action.ExecutionStatus -eq 'Pending' } |
                            Sort-Object { $_.Task."$($this.Settings.ExecutionOrder)" } |
                                Select-Object -First 1


                    ## Invoke the Most Preferred, Actionable Subject
                    if ($PreferredActionableSubject) {

                        ## Update the local cache so this task won't run again until another refresh from TM
                        $PreferredActionableSubject.Task.Status = 'Started'

                        ## Run a Subject in a normal invocation runspace, but track that task so it 'consumes' one runspace
                        if ($this.Settings.Parallel) {

                            ## Honor Throttling settings
                            if ($this.Status.ActiveSubjects.Count -lt $this.Settings.Throttle) {

                                ## Record the Task ID as belonging to this broker for Throttling
                                $this.Status.ActiveSubjects.Add($PreferredActionableSubject.Task.Id)
                                Write-Verbose "Invoking Task $($PreferredActionableSubject.Task.Id)"
                                $PreferredActionableSubject.InvokeParallel($this.TMSession.Name, $this.Cache)
                                $this.Status.TasksExecutedSinceRefresh++
                            }
                        } else {

                            ## Invoke this ActionRequest directly, in this runspace
                            $PreferredActionableSubject.Invoke($this.TMSession.Name, $this.Cache)
                            $this.Status.TasksExecutedSinceRefresh++
                        }
                    }
                }
            }

            # Refresh the progress properties to be output to the TMC UI
            $this.RefreshBrokerProgress()

            $ProgressSplat = @{
                Id              = 1
                ParentId        = 0
                Activity        = 'Subject Tasks'
                Status          = "$($this.Status.RemainingTasks.Value) remaining tasks"
                PercentComplete = $this.Status.RemainingTasks.PercentComplete
            }
            Write-Progress @ProgressSplat

            $ProgressSplat = @{
                Id              = 2
                ParentId        = 0
                Activity        = 'Timeout'
                Status          = "$($this.Status.RemainingMinutes.Value) minutes left"
                PercentComplete = $this.Status.RemainingMinutes.PercentComplete
            }
            Write-Progress @ProgressSplat

            if ($this.Settings.Parallel) {
                $ProgressSplat = @{
                    Id              = 3
                    ParentId        = 0
                    Activity        = 'Throttle'
                    Status          = "$($this.Status.ActiveSubjects.Count) of $($this.Settings.Throttle)"
                    PercentComplete = $this.Status.Throttle.PercentComplete
                }
                Write-Progress @ProgressSplat
            }

            ## Sleep, unless there are more tasks ready
            if ($this.Subjects.Task.Status -notcontains 'Ready') {
                Start-Sleep -Seconds $this.Settings.Timing.PauseSeconds

                ## Refresh the Task statuses
                $this.RefreshTaskStatuses()
                $this.RefreshBrokerProgress()
                $this.Status.TasksExecutedSinceRefresh = 0
            }
        }
    }

    #endregion Non-Static Methods

}


class TMBrokerEventData {
    [TMEvent]$Event
    [TMTask[]]$Tasks

    TMBrokerEventData() {}

    TMBrokerEventData([Int]$ProjectId, [String]$EventName, [String]$TMSession) {
        $this.GetEventData($ProjectId, $EventName, $TMSession)
    }

    [void]GetEventData([Int]$ProjectId, [String]$EventName, [String]$TMSession) {

        # Get the Event object
        $this.Event = Get-TMEvent -TMSession $TMSession -ProjectId $ProjectId -Name $EventName

        # Get all of the broker-related Tasks in the Event
        $this.Tasks = Get-TMTask -TMSession $TMSession -ProjectId $ProjectId -EventName $EventName
    }

    [void]GetEventData([Int]$ProjectId, [String]$EventName, [String]$TMSession, [TMBrokerTaskFilter]$Filter) {

        # Get the Event object
        $this.Event = Get-TMEvent -TMSession $TMSession -ProjectId $ProjectId -Name $EventName

        # Get all of the broker-related Tasks in the Event
        $TaskSplat = $Filter.ToHashTable()
        $this.Tasks = Get-TMTask -TMSession $TMSession -ProjectId $ProjectId -EventName $EventName @TaskSplat
    }
}


class TMBrokerSubjectScope {
    [ValidateSet('Service', 'Inline')]
    [String]$Type

    [ValidateSet('ActionName', 'AssetClass', 'AssetName', 'AssetType', 'Category', 'Status', 'TaskNumber', 'TaskSpecId', 'Team', 'Title')]
    hidden [String]$TaskProperty

    hidden [String[]]$MatchingCriteria

    [ScriptBlock]$MatchExpression

    [ValidateSet('TaskFilter', 'MatchExpression')]
    [String]$FilterType

    [TMBrokerTaskFilter]$TaskFilter

    hidden [String]$MatchRegexString

    static $ValidTypes = @('Service', 'Inline')
    static $ValidTaskProperties = @('ActionName', 'AssetClass', 'AssetName', 'AssetType', 'Category', 'Status', 'TaskNumber', 'TaskSpecId', 'Team', 'Title')

    TMBrokerSubjectScope() {
        $this.Type = 'Inline'
        $this.TaskFilter = [TMBrokerTaskFilter]::new()
        $this.TaskFilter.Title.Add('\[Subject\]')
        $this.FilterType = 'TaskFilter'
    }

    TMBrokerSubjectScope([String]$_type, [String]$_taskProperty, [String[]]$_matchingCriteria) {
        $this.Type = $_type
        $this.TaskProperty = $_taskProperty
        $this.MatchingCriteria = $_matchingCriteria
        $this.TaskFilter = [TMBrokerTaskFilter]::new()
        $_matchingCriteria | ForEach-Object {
            $this.TaskFilter."$_taskProperty".Add($_)
        }
        $this.FilterType = 'TaskFilter'
    }

    TMBrokerSubjectScope([String]$_type, [ScriptBlock]$_matchExpression) {
        $this.Type = $_type
        $this.MatchExpression = $_matchExpression
        $this.FilterType = 'MatchExpression'
    }

    TMBrokerSubjectScope([String]$_type, [TMBrokerTaskFilter]$_taskFilter) {
        $this.Type = $_type
        $this.TaskFilter = $_taskFilter
        $this.FilterType = 'TaskFilter'
    }

    hidden [void]GetMatchExpression() {
        $this.MatchExpression = [ScriptBlock]::Create("`$_.$($this.TaskProperty) -match '$([TMBrokerSubjectScope]::GetMatchString($this.MatchingCriteria))'")
    }

    static [String]GetMatchString([String[]]$_criteria) {
        return ('(' + ($_criteria -join ')|(') + ')')
    }
}


class TMBrokerSetting {
    [TMBrokerSubjectScope]$SubjectScope
    [TMBrokerTiming]$Timing
    [ValidateSet('TaskNumber', 'Score')]
    [String]$ExecutionOrder
    [bool]$Parallel = $false
    [Int]$Throttle = 8

    TMBrokerSetting() {
        $this.Timing = [TMBrokerTiming]::new()
        $this.SubjectScope = [TMBrokerSubjectScope]::new()
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [String]$_taskProperty,
        [String[]]$_matchingCriteria,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskProperty, $_matchingCriteria)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [ScriptBlock]$_matchExpression,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_matchExpression)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [TMBrokerTaskFilter]$_taskFilter,
        [Int]$_timeout,
        [Int]$_pauseSeconds
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskFilter)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [String]$_taskProperty,
        [String[]]$_matchingCriteria,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskProperty, $_matchingCriteria)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
        $this.Parallel = $_parallel
        $this.Throttle = $_throttle
    }

    TMBrokerSetting(
        [String]$_type,
        [ScriptBlock]$_matchExpression,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_matchExpression)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
        $this.Parallel = $_parallel
        $this.Throttle = $_throttle
    }

    TMBrokerSetting(
        [String]$_type,
        [TMBrokerTaskFilter]$_taskFilter,
        [Int]$_timeout,
        [Int]$_pauseSeconds,
        [bool]$_parallel,
        [int]$_throttle
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskFilter)
        $this.Timing = [TMBrokerTiming]::new($_timeout, $_pauseSeconds)
        $this.ExecutionOrder = 'TaskNumber'
        $this.Parallel = $_parallel
        $this.Throttle = $_throttle
    }

    TMBrokerSetting(
        [String]$_type,
        [String]$_taskProperty,
        [String[]]$_matchingCriteria
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskProperty, $_matchingCriteria)
        $this.Timing = [TMBrokerTiming]::new()
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [ScriptBlock]$_matchExpression
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_matchExpression)
        $this.Timing = [TMBrokerTiming]::new()
        $this.ExecutionOrder = 'TaskNumber'
    }

    TMBrokerSetting(
        [String]$_type,
        [TMBrokerTaskFilter]$_taskFilter
    ) {
        $this.SubjectScope = [TMBrokerSubjectScope]::new($_type, $_taskFilter)
        $this.Timing = [TMBrokerTiming]::new()
        $this.ExecutionOrder = 'TaskNumber'
    }
}


class TMBrokerTiming {
    [System.Int64]$TimeoutMinutes
    [System.Int64]$PauseSeconds
    [System.Diagnostics.Stopwatch]$Timer

    TMBrokerTiming () {
        $this.TimeoutMinutes = 120
        $this.PauseSeconds = 15
        $this.Timer = [System.Diagnostics.Stopwatch]::new()
    }

    TMBrokerTiming ([System.Int64]$_timeoutMinutes, [System.Int64]$_pauseSeconds) {
        $this.TimeoutMinutes = $_timeoutMinutes
        $this.PauseSeconds = $_pauseSeconds
        $this.Timer = [System.Diagnostics.Stopwatch]::new()
    }
}


class TMBrokerStatus {
    [System.Collections.Generic.List[Int64]]$ActiveSubjects
    [System.Int64]$WorkflowTaskCount
    [System.Int64]$TasksExecutedSinceRefresh
    [TMBrokerProgress]$RemainingTasks
    [TMBrokerProgress]$RemainingMinutes
    [TMBrokerProgress]$Throttle


    TMBrokerStatus () {
        $this.ActiveSubjects = [System.Collections.Generic.List[Int64]]::new()
        $this.TasksExecutedSinceRefresh = 0
        $this.RemainingTasks = [TMBrokerProgress]::new()
        $this.RemainingMinutes = [TMBrokerProgress]::new()
        $this.Throttle = [TMBrokerProgress]::new()
    }

    TMBrokerStatus ([System.Int32]$_timeoutMinutes) {
        $this.ActiveSubjects = [System.Collections.Generic.List[Int64]]::new()
        $this.TasksExecutedSinceRefresh = 0
        $this.RemainingTasks = [TMBrokerProgress]::new()
        $this.RemainingMinutes = [TMBrokerProgress]::new($_timeoutMinutes)
        $this.Throttle = [TMBrokerProgress]::new()
    }

    TMBrokerStatus ([System.Int32]$_timeoutMinutes, [System.Int32]$_throttle) {
        $this.ActiveSubjects = [System.Collections.Generic.List[Int64]]::new()
        $this.TasksExecutedSinceRefresh = 0
        $this.RemainingTasks = [TMBrokerProgress]::new()
        $this.RemainingMinutes = [TMBrokerProgress]::new($_timeoutMinutes)
        $this.Throttle = [TMBrokerProgress]::new($_throttle)
    }
}


class TMBrokerProgress {
    hidden [System.Int32]$_currentValue = 0
    hidden [System.Int32]$_maxValue = 1
    [System.Int32]$PercentComplete

    TMBrokerProgress() {
        $MemberSplat = @{
            Name        = 'MaxValue'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._maxValue
            }
            SecondValue = {
                param($value)
                $this._maxValue = $value
                $this.CalculatePercentComplete()
            }
        }
        $this | Add-Member @MemberSplat

        $MemberSplat = @{
            Name        = 'Value'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._currentValue
            }
            SecondValue = {
                param($value)
                $this._currentValue = $value
                $this.CalculatePercentComplete()
            }
        }
        $this | Add-Member @MemberSplat

        $this.CalculatePercentComplete()
    }

    TMBrokerProgress([System.Int32]$maxValue) {
        $MemberSplat = @{
            Name        = 'MaxValue'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._maxValue
            }
            SecondValue = {
                param($value)
                $this._maxValue = $value
                $this.CalculatePercentComplete()
            }
        }
        $this | Add-Member @MemberSplat

        $MemberSplat = @{
            Name        = 'Value'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._currentValue
            }
            SecondValue = {
                param($value)
                $this._currentValue = $value
                $this.PercentComplete = [System.Int32][Math]::Ceiling(($value / $this.MaxValue) * 100)
            }
        }
        $this | Add-Member @MemberSplat

        $this.MaxValue = $maxValue
    }

    TMBrokerProgress([System.Int32]$currentValue, [System.Int32]$maxValue) {
        $MemberSplat = @{
            Name        = 'MaxValue'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._maxValue
            }
            SecondValue = {
                param($value)
                $this._maxValue = $value
                $this.CalculatePercentComplete()
            }
        }
        $this | Add-Member @MemberSplat

        $MemberSplat = @{
            Name        = 'Value'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._currentValue
            }
            SecondValue = {
                param($value)
                $this._currentValue = $value
                $this.PercentComplete = [System.Int32][Math]::Ceiling(($value / $this.MaxValue) * 100)
            }
        }
        $this | Add-Member @MemberSplat

        $this.Value = $currentValue
        $this.MaxValue = $maxValue
    }

    [void]CalculatePercentComplete() {
        $this.PercentComplete = [System.Int32][Math]::Ceiling(($this._currentValue / $this._maxValue) * 100)
    }
}


class TMBrokerTaskFilter {
    [Collections.Generic.List[Int32]]$TaskNumber
    [Collections.Generic.List[Int32]]$TaskSpecId
    [Collections.Generic.List[String]]$Status
    [Collections.Generic.List[String]]$AssetName
    [Collections.Generic.List[String]]$AssetType
    [Collections.Generic.List[String]]$AssetClass
    [Collections.Generic.List[String]]$ActionName
    [Collections.Generic.List[String]]$Category
    [Collections.Generic.List[String]]$Title
    [Collections.Generic.List[String]]$Team

    TMBrokerTaskFilter() {
        $this.TaskNumber = [Collections.Generic.List[Int32]]::new()
        $this.TaskSpecId = [Collections.Generic.List[Int32]]::new()
        $this.Status = [Collections.Generic.List[String]]::new()
        $this.AssetName = [Collections.Generic.List[String]]::new()
        $this.AssetType = [Collections.Generic.List[String]]::new()
        $this.AssetClass = [Collections.Generic.List[String]]::new()
        $this.ActionName = [Collections.Generic.List[String]]::new()
        $this.Category = [Collections.Generic.List[String]]::new()
        $this.Title = [Collections.Generic.List[String]]::new()
        $this.Team = [Collections.Generic.List[String]]::new()
    }

    [Hashtable]ToHashTable() {
        $returnHashtable = @{}

        if ($this.TaskNumber) {$returnHashtable.TaskNumber = $this.TaskNumber}
        if ($this.TaskSpecId) {$returnHashtable.TaskSpecId = $this.TaskSpecId}
        if ($this.Status) {$returnHashtable.Status = $this.Status}
        if ($this.AssetName) {$returnHashtable.AssetName = $this.AssetName}
        if ($this.AssetType) {$returnHashtable.AssetType = $this.AssetType}
        if ($this.AssetClass) {$returnHashtable.AssetClass = $this.AssetClass}
        if ($this.ActionName) {$returnHashtable.ActionName = $this.ActionName}
        if ($this.Category) {$returnHashtable.Category = $this.Category}
        if ($this.Title) {$returnHashtable.Title = $this.Title}
        if ($this.Team) {$returnHashtable.Team = $this.Team}

        return $returnHashtable
    }

    [String]ToString() {
        return $this.ToHashTable() | ConvertTo-Json
    }
}