Search code examples
powershelleventsdebouncing

Debounce Function in PowerShell


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?


Solution

  • 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.