I'm attempting to create a simple debounce function in PowerShell. Here is what I have so far.
# timer.ps1
# Define debounce function
Function New-Debounce {
param(
[scriptblock]$ScriptBlock,
[int]$DelayMilliseconds
)
$timer = $null
$id = $null
return {
# reset timer
if ($timer) {
$timer.Stop()
$timer.Dispose()
}
$timer = [System.Timers.Timer]::new($DelayMilliseconds)
# only fire once
$timer.AutoReset = $false
$timer.Start()
# Listen for the Elapsed event and execute our script block when it occurs
$id = Register-ObjectEvent `
-InputObject $timer `
-EventName "Elapsed" `
-SourceIdentifier "Timer.Debounce" `
-Action { Write-Host "Hello!"; & $ScriptBlock }
}.GetNewClosure()
}
$f = New-Debounce -ScriptBlock { Write-Host "There" } -DelayMilliseconds 1000
& $f
# # Keep the process alive so the timer can do its thing
for ($i = 0; $i -lt 5; $i++) {
Start-Sleep -Seconds 1
}
Unregister-Event "Timer.Debounce"
Running timer.ps1 always outputs "Hello" but never "There", and I'm unsure why.
Why would my passed $ScriptBlock not be executed when the timer fires? How would you improve this code?
Use -MessageData
to pass in the $ScriptBlock
to the Action block otherwise that value is lost once your function returns.
# Listen for the Elapsed event and execute our script block when it occurs
$registerObjectEventSplat = @{
InputObject = $timer
EventName = 'Elapsed'
SourceIdentifier = 'Timer.Debounce'
MessageData = $ScriptBlock
Action = { Write-Host 'Hello!'; & $event.MessageData }
}
$id = Register-ObjectEvent @registerObjectEventSplat
The other alternative which I personally wouldn't recommend is to dot source the function call. The original code remains the same but when calling the function you just add a .
before calling it:
$f = . New-Debounce -ScriptBlock { Write-Host 'There' } -DelayMilliseconds 1000
& $f
The downside here is that first, it will be quite confusing for the reader and second is that the passed parameters ($ScriptBlock
and $DelayMilliseconds
) will now exist in the caller's execution context.