Servers/EventServer.ps1

<#
.SYNOPSIS
    Event Server
.DESCRIPTION
    A simple event driven server.

    Each request will generate an event, which will be responded to by a handler.
.EXAMPLE
    ./EventServer.ps1 ($pwd | Split-Path)
#>

param(
# The rootDirectory.
[string]$RootDirectory = $PSScriptRoot,

# The rootUrl of the server. By default, a random loopback address.
[string]$RootUrl=
    "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/",

# The type map. This determines how each extension will be served.
[Collections.IDictionary]
$TypeMap = [Ordered]@{
    ".html" = "text/html" ; ".css" = "text/css"  ; ".svg" = "image/svg+xml" ;
    ".png"  = "image/png" ; ".jpg" = "image/jpeg"; ".gif" = "image/gif"
    ".mp3"  = "audio/mpeg"; ".mp4" = "video/mp4"
    ".json" = "application/json"; ".xml"  = "application/xml" ; 
    ".js"   = "text/javascript" ; ".jsm" = "text/javascript" ;
}
)

$httpListener = [Net.HttpListener]::new()
$httpListener.Prefixes.Add($RootUrl)
Write-Warning "Listening on $RootUrl $($httpListener.Start())"

$io = [Ordered]@{ # Pack our job input into an IO dictionary
    HttpListener = $httpListener ; ServerRoot = $RootDirectory
    MainRunspace = [Runspace]::DefaultRunspace; SourceIdentifier = $RootUrl
    TypeMap = $TypeMap
}

# Our server is a thread job
Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io)
    $psvariable = $ExecutionContext.SessionState.PSVariable
    foreach ($key in $io.Keys) { # First, let's unpack.
        if ($io[$key] -is [PSVariable]) { $psvariable.set($io[$key]) }
        else { $psvariable.set($key, $io[$key]) }
    }
    
    # Listen for the next request
    :nextRequest while ($httpListener.IsListening) {     
        $getContext = $httpListener.GetContextAsync()
        while (-not $getContext.Wait(17)) { }
        $request, $reply =
            $getContext.Result.Request, $getContext.Result.Response
        # Generate an event for every request
        $mainRunspace.Events.GenerateEvent(            
            $SourceIdentifier, $httpListener, @(
                $getContext.Result, $request, $reply                    
            ), [Ordered]@{
                Method = $Request.HttpMethod; Url = $request.Url
                Request = $request; Reply = $reply; Response = $reply
                ServerRoot = $ServerRoot; TypeMap = $TypeMap
            }
        )
    }            
} -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job,
    Add-Member -NotePropertyMembers @{ # but attach a few properties first:
        HttpListener=$httpListener # * The listener (so we can stop it)
        IO=$IO # * The IO (so we can change it)
        Url="$RootUrl" # The URL (so we can easily access it).
    } -Force -PassThru # Pass all of that thru and return it to you.

# Now register a handler for these events.
Register-EngineEvent -SourceIdentifier $RootUrl -Action {
    $request = $event.MessageData.Request
    $reply = $event.MessageData.Reply
    
    $timeToRespond = [DateTime]::Now - $event.TimeGenerated
    $myReply = "$($request.HttpMethod) $($request.Url) $($timeToRespond)"
    $reply.Close($OutputEncoding.GetBytes($myReply), $false)
}

# Because events are processed on the main runspace thread, this cannot Invoke-RestMethod itself.