Private/Runtime/Invoke-ElmEventLoop.ps1

function Invoke-ElmEventLoop {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$InitialModel,

        [Parameter(Mandatory)]
        [scriptblock]$UpdateFn,

        [Parameter(Mandatory)]
        [scriptblock]$ViewFn,

        [Parameter(Mandatory)]
        [object]$InputQueue,

        [Parameter()]
        [AllowNull()]
        [scriptblock]$SubscriptionFn = $null,

        [Parameter()]
        [int]$TerminalWidth = 80,

        [Parameter()]
        [int]$TerminalHeight = 24
    )

    $esc        = [char]27
    $hideCursor = $esc + '[?25l'
    $showCursor = $esc + '[?25h'

    $model    = $InitialModel
    $prevTree = $null

    [Console]::Write($hideCursor)
    try {
        # Initial render before any messages arrive
        $viewTree     = Invoke-ElmView -ViewFn $ViewFn -Model $model
        $measuredTree = Measure-ElmViewTree -Root $viewTree -TermWidth $TerminalWidth -TermHeight $TerminalHeight
        $patches      = @(Compare-ElmViewTree -OldTree $null -NewTree $measuredTree)
        $prevTree     = $measuredTree
        if ($patches.Count -gt 0) {
            [Console]::Write((ConvertTo-AnsiOutput -Root $measuredTree))
        }

        if ($null -ne $SubscriptionFn) {
            # Subscription-based path: Invoke-ElmSubscriptions is the sole queue consumer.
            # Messages are batched; a single render happens after each batch.
            $timerState = @{}
            while ($true) {
                $subs = @(& $SubscriptionFn $model)
                $msgs = @(Invoke-ElmSubscriptions -Subscriptions $subs -InputQueue $InputQueue -TimerState $timerState)

                if ($msgs.Count -eq 0) {
                    [System.Threading.Thread]::Sleep(1)
                    continue
                }

                $shouldQuit = $false
                foreach ($msg in $msgs) {
                    $updateResult = Invoke-ElmUpdate -UpdateFn $UpdateFn -Message $msg -Model $model
                    $model = $updateResult.Model
                    $cmd   = $updateResult.Cmd
                    if ($null -ne $cmd -and $cmd.Type -eq 'Quit') {
                        $shouldQuit = $true
                        break
                    }
                }

                $viewTree     = Invoke-ElmView -ViewFn $ViewFn -Model $model
                $measuredTree = Measure-ElmViewTree -Root $viewTree -TermWidth $TerminalWidth -TermHeight $TerminalHeight
                $patches      = @(Compare-ElmViewTree -OldTree $prevTree -NewTree $measuredTree)
                $prevTree     = $measuredTree

                if ($patches.Count -gt 0) {
                    if ($patches[0].Type -eq 'FullRedraw') {
                        [Console]::Write((ConvertTo-AnsiOutput -Root $measuredTree))
                    } else {
                        [Console]::Write((ConvertTo-AnsiPatch -Patches $patches))
                    }
                }

                if ($shouldQuit) { break }
            }
        } else {
            # Legacy path: direct queue dequeue, raw messages forwarded to UpdateFn.
            while ($true) {
                $msg = $null
                if ($InputQueue.TryDequeue([ref]$msg)) {
                    $updateResult = Invoke-ElmUpdate -UpdateFn $UpdateFn -Message $msg -Model $model
                    $model = $updateResult.Model
                    $cmd   = $updateResult.Cmd

                    if ($null -ne $cmd -and $cmd.Type -eq 'Quit') {
                        break
                    }

                    $viewTree     = Invoke-ElmView -ViewFn $ViewFn -Model $model
                    $measuredTree = Measure-ElmViewTree -Root $viewTree -TermWidth $TerminalWidth -TermHeight $TerminalHeight
                    $patches      = @(Compare-ElmViewTree -OldTree $prevTree -NewTree $measuredTree)
                    $prevTree     = $measuredTree

                    if ($patches.Count -gt 0) {
                        if ($patches[0].Type -eq 'FullRedraw') {
                            $ansiOutput = ConvertTo-AnsiOutput -Root $measuredTree
                        } else {
                            $ansiOutput = ConvertTo-AnsiPatch -Patches $patches
                        }
                        [Console]::Write($ansiOutput)
                    }
                } else {
                    [System.Threading.Thread]::Sleep(1)
                }
            }
        }
    } finally {
        [Console]::Write($showCursor)
    }

    return $model
}