Private/SignalServer.ps1

using namespace Pode

function Start-PodeSignalServer
{
    # work out which endpoints to listen on
    $endpoints = @()
    @(Get-PodeEndpoints -Type Ws) | ForEach-Object {
        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1)
        $_ip = (Get-PodeIPAddress $_ip)

        # add endpoint to list
        $endpoints += @{
            Address = $_ip
            Hostname = $_.HostName
            IsIPAddress = $_.IsIPAddress
            Port = $_.Port
            Certificate = $_.Certificate.Raw
            AllowClientCertificate = $_.Certificate.AllowClientCertificate
            Url = $_.Url
        }
    }

    # create the listener
    $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token, [PodeListenerType]::WebSocket)
    $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)

    try
    {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            $socket = [PodeSocket]::new($_.Address, $_.Port, $PodeContext.Server.Sockets.Ssl.Protocols, $_.Certificate, $_.AllowClientCertificate)
            $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout

            if (!$_.IsIPAddress) {
                $socket.Hostnames.Add($_.HostName)
            }

            $listener.Add($socket)
        }

        $listener.Start()
        $PodeContext.Listeners += $listener
        $PodeContext.Server.WebSockets.Listener = $listener
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeDisposable -Disposable $listener
        throw $_.Exception
    }

    # script to write messages back to the client(s)
    $signalScript = {
        param(
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token))

                try
                {
                    # get the sockets for the message
                    $sockets = @()

                    # by clientId
                    if (![string]::IsNullOrWhiteSpace($message.ClientId)) {
                        $sockets = @($Listener.WebSockets[$message.ClientId])
                    }
                    else {
                        $sockets = @($Listener.WebSockets.Values)

                        # by path
                        if (![string]::IsNullOrWhiteSpace($message.Path)) {
                            $sockets = @(foreach ($socket in $sockets) {
                                if ($socket.Path -ieq $message.Path) {
                                    $socket
                                    break
                                }
                            })
                        }
                    }

                    # do nothing if no socket found
                    if (($null -eq $sockets) -or ($sockets.Length -eq 0)) {
                        continue
                    }

                    # send the message to all found sockets
                    foreach ($socket in $sockets) {
                        try {
                            $socket.Context.Response.SendSignal($message)
                        }
                        catch {
                            $Listener.WebSockets.Remove($socket.ClientId) | Out-Null
                        }
                    }
                }
                catch [System.OperationCanceledException] {}
                catch {
                    $_ | Write-PodeErrorLog
                    $_.Exception | Write-PodeErrorLog -CheckInnerException
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    Add-PodeRunspace -Type Signals -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener }

    # script to queue messages from clients to send back to other clients from the server
    $clientScript = {
        param(
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                $context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token))

                try
                {
                    $payload = ($context.Message | ConvertFrom-Json)
                    $Request = $context.WebSocket.Context.Request
                    $Response = $context.WebSocket.Context.Response

                    $SignalEvent = @{
                        Response = $Response
                        Request = $Request
                        Lockable = $PodeContext.Lockables.Global
                        Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath)
                        Data = @{
                            Path = [System.Web.HttpUtility]::UrlDecode($payload.path)
                            Message = $payload.message
                            ClientId = $payload.clientId
                        }
                        Endpoint = @{
                            Protocol = $Request.Url.Scheme
                            Address = $Request.Host
                            Name = $null
                        }
                        Route = $null
                        Timestamp = $context.Timestamp
                        Streamed = $true
                    }

                    # endpoint name
                    $SignalEvent.Endpoint.Name = (Find-PodeEndpointName -Protocol $SignalEvent.Endpoint.Protocol -Address $SignalEvent.Endpoint.Address -LocalAddress $SignalEvent.Request.LocalEndPoint)

                    # see if we have a route and invoke it, otherwise auto-send
                    $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name

                    if ($null -ne $SignalEvent.Route) {
                        $_args = @($SignalEvent.Route.Arguments)
                        if ($null -ne $SignalEvent.Route.UsingVariables) {
                            $_vars = @()
                            foreach ($_var in $SignalEvent.Route.UsingVariables) {
                                $_vars += ,$_var.Value
                            }
                            $_args = $_vars + $_args
                        }

                        Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $_args -Scoped -Splat
                    }
                    else {
                        Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId
                    }
                }
                catch [System.OperationCanceledException] {}
                catch {
                    $_ | Write-PodeErrorLog
                    $_.Exception | Write-PodeErrorLog -CheckInnerException
                }
                finally {
                    Update-PodeServerSignalMetrics -SignalEvent $SignalEvent
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    Add-PodeRunspace -Type Signals -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener }

    # script to keep web server listening until cancelled
    $waitScript = {
        param(
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try {
            while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeDisposable -Disposable $Listener
        }
    }

    Add-PodeRunspace -Type Signals -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
    return @($endpoints.Url)
}