Public/WebSocketClient.ps1

function Start-TMConsoleWebSocketClient {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WebSocketServer,

        [Parameter(Mandatory = $True)]
        [Int]$WebSocketPort,

        [Parameter()]$HostPID = -1,

        [Parameter(mandatory = $false)]
        [Bool]$OutputVerbose = $false,

        [Parameter()]
        [Bool]$AllowInsecureSSL = $False,

        [Parameter(Mandatory = $false)]
        [string]$WebSocketEncryptedAccessKey,

        [Parameter(Mandatory = $false)]
        [Version]$TMCVersion = '0.0.0'
    )

    begin {
        $global:AllowInsecureSSL = $AllowInsecureSSL
        $global:OutputVerbose = $OutputVerbose

        ## Enable Verbose Output if requested
        if ($global:OutputVerbose) {
            $global:VerbosePreference = 'Continue'
            $VerbosePreference = 'Continue'
            Write-Output 'Starting PowerShell Web Socket Client'
        }

        ## Define the Script Block that is used to wrap and invoke the ActionRequest
        $global:ScriptBlock_ActionRequestInnerJobScriptBlock = [scriptblock] {
            param(
                $ActionRequest,
                $AllowInsecureSSL,
                [EventQueue[psobject]]$TMConsoleActionRequestQueue
            )
            
            ## Ensure all streams are disabled
            $InformationPreference = 'SilentlyContinue'
            $VerbosePreference = 'SilentlyContinue'
            $ProgressPreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
            $WarningPreference = 'SilentlyContinue'
            $Global:InformationPreference = 'SilentlyContinue'
            $Global:VerbosePreference = 'SilentlyContinue'
            $Global:ProgressPreference = 'SilentlyContinue'
            $Global:DebugPreference = 'SilentlyContinue'
            $Global:WarningPreference = 'SilentlyContinue'
            $ErrorActionPreference = 'SilentlyContinue'

            $TaskTMSession = $null
            try {
                ## Import the TMC Action Request
                Import-TMCActionRequest -PSObjectActionRequest $ActionRequest
                $TaskTMSession = $Global:TMSessions.Default

                # ## Create a Parameters Variable from the Action Script
                New-Variable -Name Params -Scope Global -Value $ActionRequest.params -Force

                ## Enable Logging
                if ($ActionRequest.logPath) {

                    ## Trim any quote characters from the Log Path
                    $RootLogPath = $ActionRequest.logPath.trim('"').trim("'")
                    Test-FolderPath -FolderPath $RootLogPath

                    ## Create the Transcript Folder Path
                    $ProjectFolder = Join-Path -Path $RootLogPath ($Global:TM.Server.Url -replace '.transitionmanager.net', '') $Global:TM.Project.Name $Global:TM.Event.Name
                    Test-FolderPath -FolderPath $ProjectFolder

                    ## Create a unique file in the Transcript Folder
                    $TranscriptFileName = (
                        (Get-Date -Format FileDateTimeUniversal) +
                        '_TaskNumber-' + $Global:TM.Task.TaskNumber +
                        '_TaskId-' + $Global:TM.Task.Id + '.txt'
                    )
                    $TranscriptFilePath = Join-Path $ProjectFolder $TranscriptFileName

                    ## Start a transcript for this session
                    $TranscriptSplat = @{
                        Path                    = $TranscriptFilePath
                        IncludeInvocationHeader = $True
                        Confirm                 = $False
                        Force                   = $True
                        Append                  = $True
                    }
                    Start-Transcript @TranscriptSplat
                }

                ## Create a Data Options parameter for the Complete-TMTask command
                $CompleteTaskParameters = @{}

                ## Add SSL Exception if necessary
                if ($global:AllowInsecureSSL) {
                    $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True
                }

                $InvocationError = $Null
                ## Run the User Provided Script
                $InvocationErrorText = $Null
                try {
                    ## Invoke the User Script block
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script)
                    Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope

                    $Stopwatch.Stop()
                    Write-Host -Message "Task completed in: " -NoNewline
                    Write-Host -Message (Get-TimeSpanString -Timespan $Stopwatch.Elapsed) -ForegroundColor Green
                }
                catch {
                    $Stopwatch.Stop()
                    $InvocationError = $_
                    $InvocationErrorText = $_?.Exception?.Message ?? $_ ?? "Error"
                }

                ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion
                if ($Global:TMAssetUpdates) {
                    $CompleteTaskParameters['Data'] = @{
                        assetUpdates = $Global:TMAssetUpdates
                    }
                    Remove-Variable -Scope Global -Name TMAssetUpdates -Force
                }

                ## End the log for this session
                if ($ActionRequest.LogPath) {
                    Stop-Transcript
                }

                if ($InvocationErrorText) {
                    Write-Host "Action Error: $InvocationErrorText" -ForegroundColor Red
                    Set-TMTaskOnHold -ActionRequest $ActionRequest -Message ('Action Error: ' + $InvocationErrorText) @CompleteTaskParameters
                    throw $InvocationError
                }
                else {
                    try {
                        Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters
                    }
                    catch {
                        Write-Host $_
                    }
                }
            }
            finally {
                try{
                    if ($null -ne $TaskTMSession) {
                        $TaskTMSession.Dispose()

                        if (
                            $null -ne $Global:TMSessions -and
                            $Global:TMSessions.ContainsKey('Default') -and
                            [Object]::ReferenceEquals($Global:TMSessions.Default, $TaskTMSession)
                        ) {
                            $Global:TMSessions.Remove('Default')
                        }
                    }
                }
                finally {
                    $TaskTMSession = $null
                }
            }
        }
    }

    process {
        try {
            ## Define the global TMConsole dataset
            $Global:TMConsole = @{
                
                ## TMC PS Operation Queues
                ActionRequests       = New-Object 'EventQueue[PSObject]'
                TerminateTaskActions = New-Object 'EventQueue[PSObject]'
                RemoveRunspaces      = New-Object 'TMCSessionQueue[PSObject]'
                
                ## UI Message Queues - Major events
                TaskChanges          = New-Object 'TMCSessionQueue[PSObject]'
                SystemErrors         = New-Object 'TMCSessionQueue[PSObject]'
                
                ## UI Message Queues - Aggregate events
                TaskOutput           = New-Object 'TMCSessionQueue[PSObject]'
                TaskProgress         = New-Object 'TMCSessionQueue[PSObject]'

                ## Runspaces manifest
                Runspaces            = [pscustomobject]@{}
            }
            $ActionRequestEventHandler = @{
                EventName        = 'ItemEnqueued'
                InputObject      = $Global:TMConsole.ActionRequests
                SourceIdentifier = 'TMC_ActionRequest_Enqueued'
                Action           = [scriptblock] {
                    param (
                        [Parameter()]
                        [PSObject]$ActionRequest
                    )
                    ## Rename the Variable to make the invocation logic more clear
                    $TMTaskID = 'TMTaskId_' + [string]$ActionRequest.task.id
                    $TMTaskNumber = $ActionRequest.task.taskNumber.ToString()
                    
                    ## Send the StatusMessages to TMConsole
                    $Global:TMConsole.TaskChanges.Enqueue(
                        [PSCustomObject]@{
                            TMTaskId = $TMTaskId
                            Type     = 'TaskStarted'
                        }
                    )

                    $Global:TMConsole.TaskProgress.Enqueue(
                        [PSCustomObject]@{
                            TMTaskId = $TMTaskID
                            Type     = 'Progress'
                            Message  = [PSCustomObject]@{
                                ActivityId        = 0
                                ParentActivityId  = -1
                                Activity          = 'Starting Task: ' + $TMTaskNumber + ' - ' + $ActionRequest.task.title
                                CurrentOperation  = 'Starting'
                                StatusDescription = ''
                                PercentComplete   = 0
                                SecondsRemaining  = -1
                                RecordType        = 0
                            }
                        }    
                    )

                    ##
                    ## Create the Runspace
                    ##
                    $Powershell = [powershell]::Create()
                    $Powershell.RunspacePool = $Global:RunspacePool
                    
                    ## Add a $TMTaskId variable so the Pipeline knows who the output belongs to
                    [void]($Powershell.AddScript(('$TMTaskId = "{0}"' -f $TMTaskID)))
                    [void]($PowerShell.AddScript({ [TMCPSHost]::CurrentTMTaskId.Value = $ExecutionContext.SessionState.PSVariable.GetValue("TMTaskId") }))

                    [void]($Powershell.AddScript($global:ScriptBlock_ActionRequestInnerJobScriptBlock))
                    [void]($Powershell.AddArgument($ActionRequest))
                    [void]($Powershell.AddArgument($global:AllowInsecureSSL))
                    [void]($Powershell.AddArgument($global:TMConsole.ActionRequests))
                    
                    ## Start the Runspace and record the Async handle
                    try {
                        $AddMemberSplat = @{
                            InputObject      = $Global:TMConsole.Runspaces 
                            NotePropertyName = $TMTaskID 
                            Force            = $True
                        }
                        [void](Add-Member @AddMemberSplat -NotePropertyValue @{
                                TMTaskId     = $TMTaskId
                                TMTaskNumber = $TMTaskNumber
                                ErrorText    = ''
                                Runspace     = $Powershell 
                                AsyncHandle  = $Powershell.BeginInvoke()
                            }
                        )
                        [TMCRunspaceDirectory]::ActiveRunspaces[$TMTaskID] = 'Running'
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type      = 'SystemError'
                                From      = 'Starting ActionRequest RunspaceJob'
                                Message   = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error'
                                Exception = $_.Exception.Message
                            }
                        )
                    }

                }
            }
            [void](Register-ObjectEvent @ActionRequestEventHandler)

            $TerminateTaskActionEventHandler = @{
                EventName        = 'ItemEnqueued'
                InputObject      = $Global:TMConsole.TerminateTaskActions
                SourceIdentifier = 'TMC_TerminateTaskAction_Enqueued'
                Action           = [scriptblock] {
                    param($TMTask)
                    $TMTaskId = "TMTaskId_$($TMTask.task.id)"
                    try {

                        ## Send Terminated task output to the console in TMC
                        $Global:TMConsole.TaskProgress.Enqueue([PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Progress'
                                Message  = [PSCustomObject]@{
                                    ActivityId        = 0
                                    ParentActivityId  = -1
                                    Activity          = 'Task Terminated'
                                    CurrentOperation  = 'Terminated!'
                                    StatusDescription = ''
                                    PercentComplete   = -1
                                    SecondsRemaining  = -1
                                    RecordType        = 1
                                }
                            }
                        )
                        $Global:TMConsole.TaskOutput.Enqueue(
                            ## Force a line break
                            [PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Information'
                                Message  = @{
                                    Message         = ""
                                    NoNewLine       = $False
                                    ForegroundColor = 'White'
                                    BackgroundColor = 'Black'
                                }
                            }
                        )
                        $Global:TMConsole.TaskOutput.Enqueue(
                            ## Print a message that the task was terminated
                            [PSCustomObject]@{
                                TMTaskId = $TMTaskId
                                Type     = 'Information'
                                Message  = @{
                                    Message         = "Task has been terminated!"
                                    NoNewLine       = $False
                                    ForegroundColor = "Red"
                                    BackgroundColor = "Black"
                                }
                            }
                        )
                        try {
                            ## Request an early, non-blocking stop. Disposal is handled by the
                            ## RemoveRunspaces drain in the main loop.
                            [void]($Global:TMConsole.Runspaces.$TMTaskId.Runspace.BeginStop($Null, $Null))
                        }
                        catch { }
                        $Global:TMConsole.RemoveRunspaces.Enqueue($TMTaskId)
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "RSJob_TerminateTaskAction FAILED removing the RSJob $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type   = 'SystemError'
                                From   = 'Terminating Runspace'
                                Detail = 'RSJob_TerminateTaskAction FAILED removing the RSJob'
                                Error  = $_.Exception.Message
                            }
                        )
                    }
                }
            }
            [void](Register-ObjectEvent @TerminateTaskActionEventHandler)
           
            ##
            ## Prepare Runspaces for execution
            ##
            $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
            $InitialSessionState.ImportPSModule('TMD.Common')
            $InitialSessionState.ImportPSModule('TransitionManager')
            $TMCPsHost = New-Object -TypeName TMCPsHost
            $MaxRunspaces = [Math]::Min([Environment]::ProcessorCount * 8, 64)
            $global:RunspacePool = [runspacefactory]::CreateRunspacePool(
                1, 
                $MaxRunspaces,
                $InitialSessionState,
                $TMCPsHost
            )
            $global:RunspacePool.ApartmentState = "MTA"
            $global:RunspacePool.Open()

            ##
            ## Build the Web Socket Client and connect
            ##
            $global:CancellationToken = New-Object System.Threading.CancellationToken
            $global:WebSocketClient = [WatsonWebsocket.WatsonWsClient]::new($WebSocketServer, $WebSocketPort, $true)
            $WebSocketReceiveMessageEventHandler = @{
                InputObject      = $global:WebSocketClient
                EventName        = 'MessageReceived'
                SourceIdentifier = 'TMC_Websocket_MessageReceived'
                Action           = [scriptblock] {
                    try {

                        ## WebSocket text messages are UTF-8 encoded byte arrays. Decode the
                        ## complete payload at once instead of rebuilding it one byte at a time.
                        $Message = ConvertFrom-Json -InputObject (
                            [System.Text.Encoding]::UTF8.GetString($Event.SourceEventArgs.Data)
                        )

                        ## Queue the Message in the appropriate Queue
                        switch ($Message.Type) {
                            'ActionRequest' {
                                ## Run the Action Request
                                $Global:TMConsole.ActionRequests.Enqueue($Message)
                                break
                            }
                            'TerminateTaskAction' {
                                ## Run the Action Request
                                $Global:TMConsole.TerminateTaskActions.Enqueue($Message)
                                break
                            }
                        }
                    }
                    catch {
                        Write-Host -ForegroundColor "Red" "EventHandler_WebSocket_ReceiveData $_"
                        $Global:TMConsole.SystemErrors.Enqueue(
                            [PSCustomObject]@{
                                Type  = 'Debug THROWN ERROR'
                                Error = $_
                            }
                        )
                    }
                }
            }
            [void](Register-ObjectEvent @WebSocketReceiveMessageEventHandler)

            ##
            ## Establish the WebSocket connection
            ##
            try {
                Write-Verbose $WebSocketEncryptedAccessKey
                $Cookie = New-Object System.Net.Cookie
                $Cookie.Name = 'tm-access-key'
                $Cookie.Value = $WebSocketEncryptedAccessKey
                $Cookie.Domain = $WebSocketServer
                $global:WebSocketClient.AddCookie($Cookie)
                $global:WebSocketClient.Start()
            }
            catch {
                Write-Host -ForegroundColor "Red" "Connection Faulted: $($_.Exception.Message)"
                throw "Connection Faulted: $($_.Exception.Message)"
            }
            
            if (-not $global:WebSocketClient.Connected) {
                Write-Host -ForegroundColor "Red" "PowerShell was unable to connect to TMConsole WebSocketServer at $($WebSocketServer):$($WebSocketPort)"
                throw "PowerShell was unable to connect to TMConsole WebSocketServer at $($WebSocketServer):$($WebSocketPort)"
            }
            
            ## Warm up each runspace so the modules load
            1..$MaxRunspaces | ForEach-Object {
                $PrepShell = [powershell]::Create()
                $PrepShell.RunspacePool = $Global:RunspacePool
                $Null = $PrepShell.AddScript('').Invoke()
            }
            
            ##
            ## Perform primary loop,
            ## Receives ActionRequets and TerminateTask items from Angular
            ## Sends UI update messages to Angular
            ## Manages RunspacePool
            ##
            $LastBatchSend = [datetime]::UtcNow
            $LastRunningActionCount = -1
            $MinBatchSize = 5
            $MaxBatchSize = 50
            $MaxBatchLatency = 50
            $SleepInterval = 10
            $MessageBatch = [System.Collections.Generic.List[object]]@()

            do {
                
                ## Check for any completed runspaces
                foreach ($TMTaskID in $Global:TMConsole.Runspaces.PSObject.Properties.Name) {
                    $TMRunspace = $Global:TMConsole.Runspaces.$TMTaskID
                    if ($TMRunspace.AsyncHandle.IsCompleted) {
                        $InvocationStateInfo = $TMRunspace.Runspace.InvocationStateInfo
                        $InvocationErrorRecord = $TMRunspace.Runspace.Streams.Error | Select-Object -Last 1

                        ## Preserve HadErrors as the primary compatibility signal. The custom
                        ## PSHost can report an error without adding an ErrorRecord to
                        ## Streams.Error. The Failed state also identifies uncaught terminating
                        ## errors surfaced directly by the PowerShell hosting API.
                        $InvocationFailed = (
                            $TMRunspace.Runspace.HadErrors -or
                            $InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Failed
                        )

                        if ($InvocationFailed) {
                            ## Uncaught terminating errors may not be added to Streams.Error by
                            ## the hosting API, but their exception is retained as State.Reason.
                            $InvocationError = $InvocationStateInfo.Reason?.Message

                            if ([String]::IsNullOrWhiteSpace($InvocationError) -and $null -ne $InvocationErrorRecord) {
                                $InvocationError = $InvocationErrorRecord.Exception?.Message
                                if ([String]::IsNullOrWhiteSpace($InvocationError)) {
                                    $InvocationError = $InvocationErrorRecord.ToString()
                                }
                            }

                            ## Write-Host is carried on the Information stream. Keep it as a
                            ## compatibility fallback for errors rendered by the custom PSHost.
                            if (
                                [String]::IsNullOrWhiteSpace($InvocationError) -and
                                $TMRunspace.Runspace.Streams.Information.Count -gt 0
                            ) {
                                $LastInformationData = $TMRunspace.Runspace.Streams.Information[-1].MessageData
                                $InvocationError = $LastInformationData?.Message
                                if ([String]::IsNullOrWhiteSpace($InvocationError) -and $null -ne $LastInformationData) {
                                    $InvocationError = $LastInformationData.ToString()
                                }
                            }

                            if ([String]::IsNullOrWhiteSpace($InvocationError)) {
                                $InvocationError = 'The task failed without providing error details.'
                            }

                            ## Send the StatusMessages to TMConsole
                            $Global:TMConsole.TaskProgress.Enqueue([PSCustomObject]@{
                                    TMTaskId = $TMTaskId
                                    Type     = 'Progress'
                                    Message  = [PSCustomObject]@{
                                        ActivityId        = 0
                                        ParentActivityId  = -1
                                        Activity          = 'Task Failed'
                                        CurrentOperation  = 'Failed'
                                        StatusDescription = ''
                                        PercentComplete   = 100
                                        SecondsRemaining  = -1
                                        RecordType        = 2
                                    }
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue(
                                [PSCustomObject]@{
                                    TMTaskId     = $TMTaskId
                                    TMTaskNumber = $TMRunspace.TMTaskNumber
                                    Type         = 'Error'
                                    Message      = $InvocationError
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue(
                                [PSCustomObject]@{
                                    TMTaskId     = $TMTaskId
                                    TMTaskNumber = $TMRunspace.TMTaskNumber
                                    Type         = 'TaskFailed'
                                    Message      = $InvocationError
                                }
                            )
                        }
                        else {
                            $Global:TMConsole.TaskProgress.Enqueue( 
                                [PSCustomObject]@{
                                    TMTaskId = $TMTaskID
                                    Type     = 'Progress'
                                    Message  = [PSCustomObject]@{
                                        ActivityId        = 0
                                        ParentActivityId  = -1
                                        Activity          = 'Task Completed'
                                        CurrentOperation  = 'Complete'
                                        StatusDescription = ''
                                        PercentComplete   = 100
                                        SecondsRemaining  = -1
                                        RecordType        = 1
                                    }
                                }
                            )
                            $Global:TMConsole.TaskChanges.Enqueue( 
                                [PSCustomObject]@{
                                    TMTaskId = $TMTaskId
                                    Type     = 'TaskCompleted'
                                }
                            )
                        }
                        $Global:TMConsole.RemoveRunspaces.Enqueue($TMTaskId)
                    }
                }

                ## Collect all of the Task State Change tokens
                while ($Global:TMConsole.RemoveRunspaces.Count) {
                    $TMTaskId = $null
                    if ($Global:TMConsole.RemoveRunspaces.TryDequeue([ref]$TMTaskId)) {

                        ## Resolve the manifest entry that belongs to the Task Id that was just
                        ## dequeued. A Task can be enqueued for removal more than once (normal
                        ## completion and an explicit termination), so this is legitimately $null
                        ## on the second pass and there is nothing left to dispose.
                        $TMRunspace = $Global:TMConsole.Runspaces.$TMTaskId

                        if ($TMRunspace -and $TMRunspace.Runspace) {

                            ## Hold the PowerShell instance locally so every call below acts on
                            ## the Task being retired and not on whatever was last enumerated
                            $TaskPowerShell = $TMRunspace.Runspace

                            ## Close out the invocation before disposing. EndInvoke re-throws a
                            ## terminating error raised by the Action script, which has already
                            ## been reported to TMConsole, so it is discarded here.
                            try {
                                if ($TMRunspace.AsyncHandle) {
                                    if ($TMRunspace.AsyncHandle.IsCompleted) {
                                        [void]($TaskPowerShell.EndInvoke($TMRunspace.AsyncHandle))
                                    }
                                    else {
                                        ## Termination path - BeginStop was already requested by the
                                        ## TerminateTaskAction handler, so this returns promptly
                                        $TaskPowerShell.Stop()
                                    }
                                }
                            }
                            catch {
                                ## Errors are present in this output but are not ours to handle here
                            }

                            ## Release the buffered stream data this Task produced
                            try {
                                [void]($TaskPowerShell.Streams.Information.Clear())
                                [void]($TaskPowerShell.Streams.Error.Clear())
                                [void]($TaskPowerShell.Streams.Warning.Clear())
                                [void]($TaskPowerShell.Streams.Verbose.Clear())
                                [void]($TaskPowerShell.Streams.Debug.Clear())
                                [void]($TaskPowerShell.Streams.Progress.Clear())
                            }
                            catch {
                                ## Errors are present in this output but are not ours to handle here
                            }

                            ## Dispose releases the pipeline and returns the runspace to the pool
                            try {
                                [void]($TaskPowerShell.Dispose())
                            }
                            catch {
                                ## Errors are present in this output but are not ours to handle here
                            }

                            $TaskPowerShell = $null
                            $TMRunspace.Runspace = $null
                            $TMRunspace.AsyncHandle = $null
                        }

                        if ($Global:TMConsole.Runspaces.PSObject.Properties.Name -contains $TMTaskId) {
                            $Global:TMConsole.Runspaces.PSObject.Properties.Remove($TMTaskId)
                        }
                        if ([TMCRunspaceDirectory]::ActiveRunspaces.Keys -contains $TMTaskId) {
                            [void]([TMCRunspaceDirectory]::ActiveRunspaces.TryRemove($TMTaskId, [ref]$null))
                        }
                    }
                }

                ## Check if the runspace count has changed
                if ($LastRunningActionCount -ne $Global:TMConsole.Runspaces.PSObject.Properties.Name.count) {
                    $LastRunningActionCount = $Global:TMConsole.Runspaces.PSObject.Properties.Name.count
                    $ActionCountLabel = switch ($LastRunningActionCount) {
                        0 { '0 Actions Running' }
                        1 { '1 Action Running' }
                        Default { "$([int]$LastRunningActionCount) Actions Running" }
                    }   
                    [void]($MessageBatch.Add([PSCustomObject]@{
                                Type    = 'powershell-server-status'
                                Message = @{
                                    connectionStatus = 'Connected'
                                    serverName       = $WebSocketServer
                                    serverStatus     = $ActionCountLabel
                                }
                            }
                        )
                    )
                }
                
                ##
                ## Priority Messages, always get sent when available
                ##
              
                ## Collect all of the Task State Change tokens
                while ($Global:TMConsole.TaskChanges.Count) {
                    $message = $null
                    if ($Global:TMConsole.TaskChanges.TryDequeue([ref]$message)) {
                        [void]($MessageBatch.Add($message))
                    }
                }
                
                ## Collect all of the System Error tokens
                while ($Global:TMConsole.SystemErrors.Count) {
                    $message = $null
                    if ($Global:TMConsole.SystemErrors.TryDequeue([ref]$message)) {
                        [void]($MessageBatch.Add($message))
                        Write-Warning $Message
                    }
                }

                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                }

                ##
                ## Aggregate Console Messages and load in blocks
                ##
                $Now = [datetime]::UtcNow
                $Elapsed = ($Now - $LastBatchSend).TotalMilliseconds

                ## Collect ConsoleOutput (Fabricated ones from Pipeline, needs updating)
                if ($Global:TMConsole.TaskOutput.count -gt $MinBatchSize -or $Elapsed -gt $MaxBatchLatency) {
                    for ($i = 0; $i -lt $MinBatchSize; $i++) {
                        $message = $null
                        if ($Global:TMConsole.TaskOutput.TryDequeue([ref]$message)) {
                            # Write-Host ($message | ConvertTo-Json -EnumsAsStrings -Compress) -Foreground Yellow
                            [void]($MessageBatch.Add($message))
                        }
                    }
                }
                
                ## Collect Progress Output
                if (
                    ($MessageBatch.Count -gt 0) -or    
                    ($Global:TMConsole.TaskProgress.count -gt $MinBatchSize) -or 
                    ($Elapsed -gt $MaxBatchLatency)
                ) {
                    while ($Global:TMConsole.TaskProgress.Count) {
                        $message = $null
                        if ($Global:TMConsole.TaskProgress.TryDequeue([ref]$message)) {
                            [void]($MessageBatch.Add($message))
                        }
                    }
                }

                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                    $LastBatchSend = $Now
                }

                ## TMC TMCPSHost Collection
                $message = $null
                while ([TMCPSHost]::OutputQueue.TryDequeue([ref]$message)) {
                    [void]($MessageBatch.Add($message))
                    if ($MessageBatch.Count -gt $MaxBatchSize) { break }
                }
                    
                if ($MessageBatch.Count -gt 0) {
                    [void]($global:WebSocketClient.SendAsync(
                            ($MessageBatch | ConvertTo-Json -EnumsAsStrings -Compress), 
                            [System.Net.WebSockets.WebSocketMessageType]::Text, 
                            $global:CancellationToken
                        )
                    )
                    [void]($MessageBatch.Clear())
                    $LastBatchSend = $Now
                }

                Start-Sleep -Milliseconds $SleepInterval

            } until (-Not $global:WebSocketClient.Connected)
        }
        catch {
            Write-Host -ForegroundColor "Red" "FAILURE IN WSS Client"
            throw $_
        }
        finally {
            
            $EventSubscribers = Get-EventSubscriber -SourceIdentifier "TM*" | Foreach-Object
            foreach ($TMEventSubscriber in $EventSubscribers) {
                Unregister-Event -Force -SubscriptionId $TMEventSubscriber.SubscriptionId
                $TMEventSubscriber.Action.Dispose()
                $TMEventSubscriber = $null
            }
            
            ## Disconnect and close down
            if ($global:WebSocketClient) {
                if ($global:WebSocketClient.Connected) {
                    $global:WebSocketClient.Stop()
                }
                $global:WebSocketClient.Dispose()
            }
        }
    }
}