Replies: 2 comments 5 replies
-
|
Assuming you are using Unity, you could implement your own public class Debouncer : IAwaitInstruction
{
private float _remainingSeconds;
public Debouncer(float seconds)
=> Reset(_remainingSeconds);
public void Reset(float seconds)
=> _remainingSeconds = seconds;
bool IAwaitInstruction.IsCompleted()
=> (_remainingSeconds -= Time.unscaledDeltaTime) <= 0;
}You can reset it for each keystroke, and pool it if you want to avoid allocations. _debouncer = new Debouncer(0.5f);
await _debouncer;_debouncer.Reset(0.5f); |
Beta Was this translation helpful? Give feedback.
5 replies
-
|
I ended up implementing a debouncer like this at my job: /// <summary>
/// Helper type used to delay requests due to user rapid-fire input to reduce load on resources (e.g. database, server, network).
/// </summary>
public sealed class Debouncer
{
private readonly SingleOpCancelHelper _cancelHelper = new();
/// <summary>
/// Waits for the specified time.
/// The returned <see cref="Promise"/> will be canceled if another request calls <see cref="Wait(TimeSpan, CancelationToken)"/>,
/// or the <paramref name="cancelationToken"/> is canceled before the time expired.
/// </summary>
public async Promise Wait(TimeSpan waitTime, CancelationToken cancelationToken)
{
using var _ = _cancelHelper.EnterScope(cancelationToken, out cancelationToken);
await Promise.Delay(waitTime, cancelationToken).ConfigureAwait(ContinuationOptions.Foreground);
}
/// <summary>
/// Cancels the last caller of <see cref="Wait(TimeSpan, CancelationToken)"/> if it hasn't yet completed.
/// </summary>
public void Cancel() => _cancelHelper.Cancel();
}
/// <summary>
/// Helper class to facilitate cancelations for an async operation that should only be ran one at a time,
/// canceling the previous operation if a new one is started.
/// </summary>
/// <remarks>This class is not thread-safe! Please update this if thread-safety is required!</remarks>
public sealed class SingleOpCancelHelper
{
private CancelationSource _cancelationSource;
/// <summary>
/// Cancels the previous scope if it has not already completed, and gets a new scope and the associated <see cref="CancelationToken"/>.
/// </summary>
/// <param name="scopedCancelationToken">The <see cref="CancelationToken"/> that is associated with the scope.</param>
/// <returns>The scope of the cancel helper that should be disposed when the async op is complete.</returns>
public Scope EnterScope(out CancelationToken scopedCancelationToken)
=> EnterScope(CancelationToken.None, out scopedCancelationToken);
/// <summary>
/// Cancels the previous scope if it has not already completed, and gets a new scope and the associated <see cref="CancelationToken"/>.
/// </summary>
/// <param name="cancelationToken">An additional <see cref="CancelationToken"/> that may be used to cancel the scope.</param>
/// <param name="scopedCancelationToken">The <see cref="CancelationToken"/> that is associated with the scope.</param>
/// <returns>The scope of the cancel helper that should be disposed when the async op is complete.</returns>
public Scope EnterScope(CancelationToken cancelationToken, out CancelationToken scopedCancelationToken)
=> EnterScope(cancelationToken, CancelationToken.None, out scopedCancelationToken);
/// <summary>
/// Cancels the previous scope if it has not already completed, and gets a new scope and the associated <see cref="CancelationToken"/>.
/// </summary>
/// <param name="token1">An additional <see cref="CancelationToken"/> that may be used to cancel the scope.</param>
/// <param name="token2">A second additional <see cref="CancelationToken"/> that may be used to cancel the scope.</param>
/// <param name="scopedCancelationToken">The <see cref="CancelationToken"/> that is associated with the scope.</param>
/// <returns>The scope of the cancel helper that should be disposed when the async op is complete.</returns>
public Scope EnterScope(CancelationToken token1, CancelationToken token2, out CancelationToken scopedCancelationToken)
{
var oldSource = _cancelationSource;
var newSource = CancelationSource.New(token1, token2);
_cancelationSource = newSource;
if (oldSource != default)
{
oldSource.Cancel();
}
scopedCancelationToken = newSource.Token;
return new Scope(this, newSource);
}
/// <summary>
/// Cancels the previous scope if it has not already completed.
/// </summary>
public void Cancel()
{
var oldSource = _cancelationSource;
if (oldSource != default)
{
_cancelationSource = default;
oldSource.Cancel();
}
}
/// <summary>
/// The scope of the cancel helper that should be disposed when the async op is complete.
/// </summary>
public readonly struct Scope : IDisposable
{
private readonly SingleOpCancelHelper _owner;
private readonly CancelationSource _cancelationSource;
internal Scope(SingleOpCancelHelper owner, CancelationSource cancelationSource)
{
_owner = owner;
_cancelationSource = cancelationSource;
}
/// <summary>
/// Releases the scope.
/// </summary>
public void Dispose()
{
if (_owner._cancelationSource == _cancelationSource)
{
_owner._cancelationSource = default;
}
_cancelationSource.Dispose();
}
}
}Used like so: private async void OnTextInputChanged(string input)
{
var cancelationToken = this.GetDestroyToken();
await _debouncer.Wait(TimeSpan.FromMilliseconds(500), cancelationToken);
// Do expensive work here.
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Hi there,
Could you please advise on how to implement a debounce method without any allocations?
https://angular.io/guide/http-optimize-server-interaction
I'm using it for an autocomplete feature, so I need only the last event after a specified delay to trigger the listener. I tried using UniTask but wasn't successful, and unfortunately, I haven't received any responses there, and even AI-generated code didn't work correctly.
Cysharp/UniTask#583
Thanks
Beta Was this translation helpful? Give feedback.
All reactions