Classes/RealtimeSink.ps1

# SignalR callbacks run on arbitrary threads without a PowerShell runspace. This compiled
# bridge only queues immutable payloads; Receive-* remains the single PowerShell consumer.
if (-not ('MyWebApi.RealtimeSink' -as [type])) {
    $libDir = Join-Path $PSScriptRoot '..' 'lib'
    $signalRRefs = @(
        (Join-Path $libDir 'Microsoft.AspNetCore.SignalR.Client.Core.dll'),
        (Join-Path $libDir 'Microsoft.AspNetCore.SignalR.Client.dll')
    )
    $signalRRefsPresent = $true
    foreach ($ref in $signalRRefs) {
        if (-not (Test-Path $ref)) { $signalRRefsPresent = $false; break }
    }

    if ($signalRRefsPresent) {
        [void][System.Collections.Concurrent.BlockingCollection[object]]
        [void][System.Text.Json.JsonElement]
        [void][System.Threading.Interlocked]
        [void][System.Threading.Volatile]
        $frameworkRefs = @('System.Collections.Concurrent', 'System.Text.Json', 'System.Threading', 'System.Runtime')
        try {
            Add-Type -CompilerOptions '-nowarn:1701,1702' -ReferencedAssemblies ($frameworkRefs + $signalRRefs) -TypeDefinition @'
using System;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;

namespace MyWebApi
{
    public sealed class RealtimeSink : IDisposable
    {
        private readonly BlockingCollection<object> _queue = new BlockingCollection<object>(1024);
        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly ConcurrentBag<Task> _streamTasks = new ConcurrentBag<Task>();
        private Exception _fault;
        private int _stopped;
        private int _disposed;

        public HubConnection Connection { get; }
        public Exception Fault => Volatile.Read(ref _fault);
        public bool HasFault => Fault != null;

        public RealtimeSink(HubConnection connection)
        {
            Connection = connection ?? throw new ArgumentNullException(nameof(connection));
            Connection.Closed += error =>
            {
                if (error != null && !_cts.IsCancellationRequested) RecordFault(error, "connection closed");
                return Task.CompletedTask;
            };
        }

        private void RecordFault(Exception error, string source)
        {
            if (error == null || _cts.IsCancellationRequested) return;
            Interlocked.CompareExchange(ref _fault, new InvalidOperationException(source + ": " + error.Message, error), null);
            try { _queue.TryAdd(new PayloadEnvelope { Method = "__connectionFault", Json = "{}", Error = Fault }, 100); }
            catch (InvalidOperationException) { }
        }

        private void Enqueue(PayloadEnvelope payload)
        {
            try { _queue.TryAdd(payload, 100, _cts.Token); }
            catch (OperationCanceledException) { }
            catch (InvalidOperationException) { }
        }

        public void On(string method)
        {
            Connection.On<JsonElement>(method, arg =>
            {
                if (_cts.IsCancellationRequested) return;
                try { Enqueue(new PayloadEnvelope { Method = method, Json = arg.GetRawText() }); }
                catch (Exception error) { RecordFault(error, "callback " + method); }
            });
        }

        public void StartStream(string method)
        {
            var task = Task.Run(async () =>
            {
                try
                {
                    await foreach (var item in Connection.StreamAsync<JsonElement>(method, _cts.Token))
                    {
                        Enqueue(new PayloadEnvelope { Method = method, Json = item.GetRawText() });
                    }
                }
                catch (OperationCanceledException) when (_cts.IsCancellationRequested) { }
                catch (Exception error) { RecordFault(error, "stream " + method); }
            }, _cts.Token);
            _streamTasks.Add(task);
        }

        public bool TryTake(out object item, int timeoutMs) => _queue.TryTake(out item, timeoutMs, _cts.Token);

        public Task StartAsync() => Connection.StartAsync(_cts.Token);

        public Task StopAsync() => StopAsync(30000);

        public async Task StopAsync(int timeoutMilliseconds)
        {
            if (Interlocked.Exchange(ref _stopped, 1) == 1) return;
            _cts.Cancel();
            Exception failure = null;
            try { await Connection.StopAsync().WaitAsync(TimeSpan.FromMilliseconds(timeoutMilliseconds)); }
            catch (Exception error) { failure = error; RecordFault(error, "connection stop"); }
            try
            {
                var allStreams = Task.WhenAll(_streamTasks);
                await allStreams.WaitAsync(TimeSpan.FromMilliseconds(timeoutMilliseconds));
            }
            catch (Exception error) { failure ??= error; RecordFault(error, "stream stop"); }
            try { await Connection.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromMilliseconds(timeoutMilliseconds)); }
            catch (Exception error) { failure ??= error; RecordFault(error, "connection dispose"); }
            _queue.CompleteAdding();
            if (failure != null) throw failure;
        }

        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
            _cts.Cancel();
            _queue.CompleteAdding();
            _cts.Dispose();
            _queue.Dispose();
        }
    }

    public sealed class PayloadEnvelope
    {
        public string Method { get; set; } = "";
        public string Json { get; set; } = "";
        public Exception Error { get; set; }
    }
}
'@

        } catch {
            throw "MyWebApi real-time assemblies are incompatible with this PowerShell/.NET runtime: $($_.Exception.Message)"
        }
    }
}