FilterBox/SRxChangeQueue.psm1
|
<# ================================================================================ SRxChangeQueue.psm1 ----------------------------------------------------------------------------- Self-contained DEFERRED CHANGE QUEUE for customizations (Round 4, item 4). Encapsulated so neither TemplateExplorer nor SRxFilterBoxControl grows. Implements the architecture doc's model: * KEYED DICTIONARY (not a raw queue) keyed by "<xPath>|<optype>|<scope>" -> free de-duplication: the last edit for a node overwrites the entry. * Ordered by a monotonic Seq (insertion order preserved for commit + review). * Owns its own commit orchestration: starts one isolated worker per entry, polls them, reports progress, and drops entries as they succeed. Host contract (all optional scriptblocks): OnStatus($text) - status bar text OnCountChanged($count) - queue size changed (show/hide Save button) OnEntryCommitted($entry,$state) - a single entry finished (success or not) OnAllCompleted($ok,$failed) - the whole batch finished Commit needs a pack builder supplied by the host, because only the host can walk the XML tree ancestry: $packBuilder = { param($entry) ... BranchPack } Requires SRxFilterBox.psm1 (Start/Get/Stop-SRxFilterBranchOperation). ================================================================================ #> Import-Module (Join-Path $PSScriptRoot 'SRxFilterBox.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue class SRxChangeQueue { hidden [System.Collections.Hashtable] $_entries = @{} # key -> entry hidden [int] $_seq = 0 # ---- commit machinery ---- hidden [System.Windows.Forms.Timer] $_timer = $null hidden [System.Collections.Hashtable] $_active = @{} # pid -> @{ Entry; Op } 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)" } }) } # ================================================================ identity static [string] MakeKey([string]$xPath, [int]$optype, [int]$scope) { return ("{0}|{1}|{2}" -f $xPath, $optype, $scope) } # ================================================================ mutation # $kind: 'Include' | 'Exclude' | 'Reset' | 'Modified' [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 } # only notify when the queue SHAPE changed (new entry, or kind changed) 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() } # ================================================================ query [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 } # Overlay for recolouring: xPath -> pending kind, for ONE layer. [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 } # ================================================================ commit # $packBuilder: { param($entry) -> BranchPack hashtable (or $null to skip) } [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) # committed -> leaves the queue } else { $this._failCount++ $rec.Entry.Error = [string]$st.Error # stays queued, marked } 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 { } } # ================================================================ events 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 |