// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System.Threading; using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Utility { /// /// Restricts the invocation of an operation to a specified time /// interval. Can also cause previous requests to be cancelled /// by new requests within that time window. Typically used for /// buffering information for an operation or ensuring that an /// operation only runs after some interval. /// /// The argument type for the Invoke method. public abstract class AsyncDebouncer { #region Private Fields private int flushInterval; private bool restartOnInvoke; private Task currentTimerTask; private CancellationTokenSource timerCancellationSource; private AsyncLock asyncLock = new AsyncLock(); #endregion #region Public Methods /// /// Creates a new instance of the AsyncDebouncer class with the /// specified flush interval. If restartOnInvoke is true, any /// calls to Invoke will cancel previous calls which have not yet /// passed the flush interval. /// /// /// A millisecond interval to use for flushing prior Invoke calls. /// /// /// If true, Invoke calls will reset prior calls which haven't passed the flush interval. /// public AsyncDebouncer(int flushInterval, bool restartOnInvoke) { this.flushInterval = flushInterval; this.restartOnInvoke = restartOnInvoke; } /// /// Invokes the debouncer with the given input. The debouncer will /// wait for the specified interval before calling the Flush method /// to complete the operation. /// /// /// The argument for this implementation's Invoke method. /// /// A Task to be awaited until the Invoke is queued. public async Task Invoke(TInvokeArgs invokeArgument) { using (await this.asyncLock.LockAsync()) { // Invoke the implementor await this.OnInvoke(invokeArgument); // If there's no timer, start one if (this.currentTimerTask == null) { this.StartTimer(); } else if (this.currentTimerTask != null && this.restartOnInvoke) { // Restart the existing timer if (this.CancelTimer()) { this.StartTimer(); } } } } /// /// Flushes the latest state regardless of the current interval. /// An AsyncDebouncer MUST NOT invoke its own Flush method otherwise /// deadlocks could occur. /// /// A Task to be awaited until Flush completes. public async Task Flush() { using (await this.asyncLock.LockAsync()) { // Cancel the current timer this.CancelTimer(); // Flush the current output await this.OnFlush(); } } #endregion #region Abstract Methods /// /// Implemented by the subclass to take the argument for the /// future operation that will be performed by OnFlush. /// /// /// The argument for this implementation's OnInvoke method. /// /// A Task to be awaited for the invoke to complete. protected abstract Task OnInvoke(TInvokeArgs invokeArgument); /// /// Implemented by the subclass to complete the current operation. /// /// A Task to be awaited for the operation to complete. protected abstract Task OnFlush(); #endregion #region Private Methods private void StartTimer() { this.timerCancellationSource = new CancellationTokenSource(); this.currentTimerTask = Task.Delay(this.flushInterval, this.timerCancellationSource.Token) .ContinueWith( t => { if (!t.IsCanceled) { return this.Flush(); } else { return Task.FromResult(true); } }); } private bool CancelTimer() { if (this.timerCancellationSource != null) { // Attempt to cancel the timer task this.timerCancellationSource.Cancel(); } // Was the task cancelled? bool wasCancelled = this.currentTimerTask == null || this.currentTimerTask.IsCanceled; // Clear the current task so that another may be created this.currentTimerTask = null; return wasCancelled; } #endregion } }