FilterBox/SRxChangeQueue.psm1

Import-Module (Join-Path $PSScriptRoot 'SRxFilterBox.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue

class SRxChangeQueue {

    hidden [System.Collections.Hashtable] $_entries = @{}
    hidden [int] $_seq = 0

    hidden [System.Windows.Forms.Timer] $_timer = $null
    hidden [System.Collections.Hashtable] $_active = @{}
    hidden [int] $_okCount = 0
    hidden [int] $_failCount = 0

    [string] $SourcePath = ''
    [scriptblock] $OnStatus = $null
    [scriptblock] $OnCountChanged = $null
    [scriptblock] $OnEntryCommitted = $null
    [scriptblock] $OnAllCompleted = $null

    SRxChangeQueue() {
        $this._timer = [System.Windows.Forms.Timer]::new()
        $this._timer.Interval = 500
        $this._timer.Tag = $this
        $this._timer.add_tick({
            param($s,$e)
            $ErrorActionPreference = 'Continue'
            try { $s.Tag.pollCommits() } catch { Write-Host "SRxChangeQueue tick: $($_.Exception.Message)" }
        })
    }

    static [string] MakeKey([string]$xPath, [int]$optype, [int]$scope) {
        return ("{0}|{1}|{2}" -f $xPath, $optype, $scope)
    }

    [void] Upsert([string]$xPath, [int]$scope, [int]$optype, [string]$kind, [hashtable]$attributes, $node) {
        try {
            if ([string]::IsNullOrEmpty($xPath)) { return }
            $key = [SRxChangeQueue]::MakeKey($xPath, $optype, $scope)

            $before   = $this._entries.Count
            $prevKind = $null
            if ($this._entries.ContainsKey($key)) { $prevKind = [string]$this._entries[$key].Kind }

            $seq = $this._seq
            if ($this._entries.ContainsKey($key)) { $seq = [int]$this._entries[$key].Seq }
            else { $this._seq++; $seq = $this._seq }

            $attrs = @{}
            if ($attributes) { foreach ($k in $attributes.Keys) { $attrs[[string]$k] = [string]$attributes[$k] } }

            $this._entries[$key] = [pscustomobject]@{
                Key        = $key
                XPath      = $xPath
                Scope      = $scope
                Optype     = $optype
                Kind       = $kind
                Attributes = $attrs
                Node       = $node
                Origin     = 'user'
                Seq        = $seq
                Error      = $null
            }

            if (($this._entries.Count -ne $before) -or ($prevKind -ne $kind)) { $this.raiseCount() }
        } catch { Write-Host "SRxChangeQueue.Upsert - $($_.Exception.Message)" }
    }

    [bool] RemoveNode([string]$xPath, [int]$scope, [int]$optype) {
        $key = [SRxChangeQueue]::MakeKey($xPath, $optype, $scope)
        if ($this._entries.ContainsKey($key)) {
            [void]$this._entries.Remove($key)
            $this.raiseCount()
            return $true
        }
        return $false
    }

    [void] Clear() { $this._entries.Clear(); $this.raiseCount() }

    [int] Count() { return $this._entries.Count }

    [object[]] Entries() {
        return @($this._entries.Values | Sort-Object { [int]$_.Seq })
    }

    [object] GetNode([string]$xPath, [int]$scope, [int]$optype) {
        $key = [SRxChangeQueue]::MakeKey($xPath, $optype, $scope)
        if ($this._entries.ContainsKey($key)) { return $this._entries[$key] }
        return $null
    }

    [hashtable] OverlayForScope([int]$scope) {
        $map = @{}
        foreach ($e in $this._entries.Values) {
            if ([int]$e.Scope -ne $scope) { continue }
            $map[[string]$e.XPath] = [pscustomobject]@{
                Exclude  = ($e.Kind -eq 'Exclude')
                Include  = ($e.Kind -eq 'Include')
                Modified = ($e.Kind -eq 'Modified')
                Reset    = ($e.Kind -eq 'Reset')
            }
        }
        return $map
    }

    [void] CommitAll($packBuilder) {
        try {
            if ($this._entries.Count -eq 0) { return }
            if (-not $packBuilder) { $this.status('Cannot commit: no pack builder supplied.'); return }
            if ($this._active.Count -gt 0) { $this.status('Commit already in progress...'); return }

            $this._okCount = 0
            $this._failCount = 0
            $started = 0

            foreach ($entry in $this.Entries()) {
                $pack = $null
                try { $pack = & $packBuilder $entry } catch { $pack = $null }
                if ($null -eq $pack) {
                    $entry.Error = 'pack build failed'
                    $this._failCount++
                    continue
                }
                $op = Start-SRxFilterBranchOperation -Pack ([hashtable]$pack) -SourcePath $this.SourcePath -HideWindow
                if ($op.StartError) {
                    $entry.Error = $op.StartError
                    $this._failCount++
                    continue
                }
                $this._active[[string]$op.ProcessId] = @{ Entry = $entry; Op = $op }
                $started++
            }

            if ($started -gt 0) {
                $this.status("Committing $started customization(s)...")
                if (-not $this._timer.Enabled) { $this._timer.Start() }
            } else {
                $this.status("Nothing committed ($($this._failCount) failed to start).")
                $this.raiseAllCompleted()
            }
        } catch { Write-Host "SRxChangeQueue.CommitAll - $($_.Exception.Message)" }
    }

    hidden [void] pollCommits() {
        if ($this._active.Count -eq 0) { $this._timer.Stop(); $this.raiseAllCompleted(); return }
        $done = @()
        foreach ($kv in @($this._active.GetEnumerator())) {
            $rec = $kv.Value
            $st = $null
            try { $st = Get-SRxFilterBranchOperationState -Operation $rec.Op } catch { }
            if ($null -eq $st) { continue }
            if ($st.State -eq 'Running') { continue }

            if ($st.State -eq 'Completed' -and $st.Success) {
                $this._okCount++
                [void]$this._entries.Remove($rec.Entry.Key)
            } else {
                $this._failCount++
                $rec.Entry.Error = [string]$st.Error
            }
            try { Remove-SRxFilterBranchTempFiles -Operation $rec.Op } catch { }
            if ($this.OnEntryCommitted) { try { & $this.OnEntryCommitted $rec.Entry $st } catch { } }
            $done += $kv.Key
        }
        foreach ($k in $done) { [void]$this._active.Remove($k) }
        $this.raiseCount()

        if ($this._active.Count -eq 0) {
            $this._timer.Stop()
            $this.raiseAllCompleted()
        }
    }

    [void] Shutdown() {
        try {
            if ($this._timer) { $this._timer.Stop() }
            foreach ($kv in @($this._active.GetEnumerator())) {
                try { Stop-SRxFilterBranchOperation -Operation $kv.Value.Op | Out-Null } catch { }
            }
            $this._active.Clear()
        } catch { }
    }

    hidden [void] status([string]$t) { if ($this.OnStatus) { try { & $this.OnStatus $t } catch { } } }
    hidden [void] raiseCount() { if ($this.OnCountChanged) { try { & $this.OnCountChanged $this._entries.Count } catch { } } }
    hidden [void] raiseAllCompleted() {
        $this.status("Commit finished: $($this._okCount) saved, $($this._failCount) failed.")
        if ($this.OnAllCompleted) { try { & $this.OnAllCompleted $this._okCount $this._failCount } catch { } }
    }
}

function New-SRxChangeQueue {
    [OutputType([object])]
    param()
    return [SRxChangeQueue]::new()
}

Export-ModuleMember -Function New-SRxChangeQueue