Skip to content

Commit 7cb1c18

Browse files
committed
Change: Refactor and clean up debouncers and related tests
1 parent 233d17c commit 7cb1c18

File tree

18 files changed

+353
-222
lines changed

18 files changed

+353
-222
lines changed
Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,62 @@
11
// ---------------------------------------------------------------------------------------------------------------------
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
4-
using CodeOfChaos.Extensions.Debouncers;
4+
using Microsoft.AspNetCore.Components;
55

66
// ReSharper disable once CheckNamespace
7-
namespace Microsoft.AspNetCore.Components;
7+
namespace CodeOfChaos.Extensions.Debouncers;
88
// ---------------------------------------------------------------------------------------------------------------------
99
// Code
1010
// ---------------------------------------------------------------------------------------------------------------------
11-
public class EventCallbackDebouncer(EventCallback callback, int debounceMs = DebouncerBase.DefaultDebounceMs)
12-
: DebouncerBase(debounceMs) {
13-
14-
protected async override ValueTask InvokeCallbackAsync(CancellationToken ct = default)
15-
=> await callback.InvokeAsync();
11+
public sealed class EventCallbackDebouncer : DebouncerBase<EventCallbackDebouncer.EmptyUnit> {
12+
public readonly struct EmptyUnit;
13+
private EventCallback Callback { get; init; }
14+
15+
// -----------------------------------------------------------------------------------------------------------------
16+
// Constructors
17+
// -----------------------------------------------------------------------------------------------------------------
18+
public static EventCallbackDebouncer FromEventCallback(EventCallback callback, int debounceMs = DefaultDebounceMs)
19+
=> new() {
20+
Callback = callback,
21+
DebounceMs = debounceMs
22+
};
1623

24+
// -----------------------------------------------------------------------------------------------------------------
25+
// Methods
26+
// -----------------------------------------------------------------------------------------------------------------
1727
public Task InvokeDebouncedAsync(CancellationToken ct = default)
1828
=> DebouncerLogicAsync(default, ct);
29+
30+
protected async override ValueTask InvokeCallbackAsync(EmptyUnit item, CancellationToken ct = default) {
31+
if (ct.IsCancellationRequested) return;
32+
await Callback.InvokeAsync();
33+
}
1934
}
2035

21-
public class EventCallbackDebouncer<T>(EventCallback<T> callback, int debounceMs = EventCallbackDebouncer<T>.DefaultDebounceMs)
22-
: DebouncerBase<T>(debounceMs) {
23-
24-
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default)
25-
=> await callback.InvokeAsync(item);
36+
public sealed class EventCallbackDebouncer<T> : DebouncerBase<T> {
37+
private EventCallback<T> Callback { get; init; }
2638

27-
public Task InvokeDebouncedAsync(CancellationToken ct = default)
39+
// -----------------------------------------------------------------------------------------------------------------
40+
// Constructors
41+
// -----------------------------------------------------------------------------------------------------------------
42+
public static EventCallbackDebouncer<T> FromEventCallback(EventCallback<T> callback, int debounceMs = DefaultDebounceMs)
43+
=> new() {
44+
Callback = callback,
45+
DebounceMs = debounceMs
46+
};
47+
48+
// -----------------------------------------------------------------------------------------------------------------
49+
// Methods
50+
// -----------------------------------------------------------------------------------------------------------------
51+
// EventCallbacks can also be invoked without the T parameter
52+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
2853
=> DebouncerLogicAsync(default, ct);
29-
54+
3055
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
3156
=> DebouncerLogicAsync(item, ct);
57+
58+
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default) {
59+
if (ct.IsCancellationRequested) return;
60+
await Callback.InvokeAsync(item);
61+
}
3262
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// ---------------------------------------------------------------------------------------------------------------------
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
4+
using CodeOfChaos.Extensions.Debouncers;
45

56
// ReSharper disable once CheckNamespace
67
namespace Microsoft.AspNetCore.Components;
7-
88
// ---------------------------------------------------------------------------------------------------------------------
99
// Code
1010
// ---------------------------------------------------------------------------------------------------------------------
1111
public static class EventCallbackExtensions {
12-
public static EventCallbackDebouncer<T> GetDebouncer<T>(this EventCallback<T> callback, int debounceMs) => new(callback, debounceMs);
13-
public static EventCallbackDebouncer GetDebouncer(this EventCallback callback, int debounceMs) => new(callback, debounceMs);
12+
public static EventCallbackDebouncer<T> GetDebouncer<T>(this EventCallback<T> callback, int debounceMs) => EventCallbackDebouncer<T>.FromEventCallback(callback, debounceMs);
13+
public static EventCallbackDebouncer GetDebouncer(this EventCallback callback, int debounceMs) => EventCallbackDebouncer.FromEventCallback(callback, debounceMs);
1414
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=debouncers_005Cregular/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=debouncers_005Cthrottled/@EntryIndexedValue">True</s:Boolean>
24
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=regex/@EntryIndexedValue">True</s:Boolean>
35
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=regularexpressions/@EntryIndexedValue">True</s:Boolean>
46
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=spanlinq/@EntryIndexedValue">False</s:Boolean></wpf:ResourceDictionary>

src/CodeOfChaos.Extensions/Debouncers/ActionDebouncer.cs

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/CodeOfChaos.Extensions/Debouncers/ActionExtensions.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
namespace CodeOfChaos.Extensions.Debouncers;
5+
6+
// ---------------------------------------------------------------------------------------------------------------------
7+
// Code
8+
// ---------------------------------------------------------------------------------------------------------------------
9+
public sealed class Debouncer: DebouncerBase<Debouncer.EmptyUnit> {
10+
public readonly struct EmptyUnit; // Workaround for reusing DebouncerBase<T> instead of creating a fully new DebouncerBase without generics
11+
private object Callback { get; init; } = null!;
12+
13+
// -----------------------------------------------------------------------------------------------------------------
14+
// Constructors
15+
// -----------------------------------------------------------------------------------------------------------------
16+
public static Debouncer FromDelegate(Action action, int debounceMs = DefaultDebounceMs)
17+
=> new() {
18+
Callback = action,
19+
DebounceMs = debounceMs
20+
};
21+
22+
public static Debouncer FromDelegate(Func<CancellationToken, Task> func, int debounceMs = DefaultDebounceMs)
23+
=> new() {
24+
Callback = func,
25+
DebounceMs = debounceMs
26+
};
27+
28+
public static Debouncer FromDelegate(Func<Task> func, int debounceMs = DefaultDebounceMs)
29+
=> new() {
30+
Callback = func,
31+
DebounceMs = debounceMs
32+
};
33+
34+
// -----------------------------------------------------------------------------------------------------------------
35+
// Methods
36+
// -----------------------------------------------------------------------------------------------------------------
37+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
38+
=> DebouncerLogicAsync(default, ct);
39+
40+
protected async override ValueTask InvokeCallbackAsync(EmptyUnit item, CancellationToken ct = default) {
41+
if (ct.IsCancellationRequested) return;
42+
43+
switch (Callback) {
44+
45+
case Action action: {
46+
action.Invoke();
47+
break;
48+
}
49+
50+
case Func<Task> func: {
51+
await func.Invoke();
52+
break;
53+
}
54+
55+
case Func<CancellationToken, Task> func: {
56+
await func.Invoke(ct);
57+
break;
58+
}
59+
60+
default: throw new InvalidOperationException("Invalid function type");
61+
}
62+
}
63+
}
64+
65+
public sealed class Debouncer<T> : DebouncerBase<T> {
66+
private object Callback { get; init; } = null!;
67+
68+
// -----------------------------------------------------------------------------------------------------------------
69+
// Constructors
70+
// -----------------------------------------------------------------------------------------------------------------
71+
private Debouncer() {}
72+
public static Debouncer<T> FromDelegate(Action<T> action, int debounceMs = DefaultDebounceMs)
73+
=> new() {
74+
Callback = action,
75+
DebounceMs = debounceMs
76+
};
77+
78+
public static Debouncer<T> FromDelegate(Func<T, Task> func, int debounceMs = DefaultDebounceMs)
79+
=> new() {
80+
Callback = func,
81+
DebounceMs = debounceMs
82+
};
83+
84+
public static Debouncer<T> FromDelegate(Func<T, CancellationToken, Task> func, int debounceMs = DefaultDebounceMs)
85+
=> new() {
86+
Callback = func,
87+
DebounceMs = debounceMs
88+
};
89+
90+
// -----------------------------------------------------------------------------------------------------------------
91+
// Methods
92+
// -----------------------------------------------------------------------------------------------------------------
93+
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
94+
=> DebouncerLogicAsync(item, ct);
95+
96+
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default) {
97+
if (ct.IsCancellationRequested) return;
98+
99+
switch (Callback) {
100+
case Action<T> action: {
101+
action.Invoke(item);
102+
break;
103+
}
104+
105+
case Func<T, Task> func: {
106+
await func.Invoke(item);
107+
break;
108+
}
109+
110+
case Func<T, CancellationToken, Task> func: {
111+
await func.Invoke(item, ct);
112+
break;
113+
}
114+
115+
default: throw new InvalidOperationException("Invalid function type");
116+
}
117+
}
118+
}

src/CodeOfChaos.Extensions/Debouncers/DebouncerBase.cs renamed to src/CodeOfChaos.Extensions/Debouncers/Regular/DebouncerBase.cs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,19 @@
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
44
namespace CodeOfChaos.Extensions.Debouncers;
5+
56
// ---------------------------------------------------------------------------------------------------------------------
67
// Code
78
// ---------------------------------------------------------------------------------------------------------------------
8-
public abstract class DebouncerBase(int debounceMs) : DebouncerBase<DebouncerBase.EmptyUnit>(debounceMs) {
9-
public readonly struct EmptyUnit;
10-
11-
// -----------------------------------------------------------------------------------------------------------------
12-
// Methods
13-
// -----------------------------------------------------------------------------------------------------------------
14-
protected abstract ValueTask InvokeCallbackAsync(CancellationToken ct = default);
15-
protected override ValueTask InvokeCallbackAsync(EmptyUnit unit, CancellationToken ct = default) => InvokeCallbackAsync(ct);
16-
}
17-
18-
public abstract class DebouncerBase<T>(int debounceMs) : IAsyncDisposable {
9+
public abstract class DebouncerBase<T> : IAsyncDisposable {
1910
protected const int DefaultDebounceMs = 100;
11+
protected int DebounceMs { get; init; }
2012

2113
private readonly SemaphoreSlim _semaphore = new(1, 1);
2214
private CancellationTokenSource? _cts;
2315
private Task? _debounceTask;
2416
private bool _isDisposed;
2517
private T? _latestValue;
26-
private DateTime _lastInvokeTime = DateTime.MinValue;
2718

2819
// -----------------------------------------------------------------------------------------------------------------
2920
// Methods
@@ -49,15 +40,12 @@ protected async Task DebouncerLogicAsync(T? value = default, CancellationToken c
4940

5041
_debounceTask = Task.Run(function: async () => {
5142
try {
52-
await Task.Delay(debounceMs, debounceToken);
53-
54-
if (debounceToken.IsCancellationRequested) return;
43+
await Task.Delay(DebounceMs, debounceToken);
5544

56-
DateTime now = DateTime.UtcNow;
57-
if (!((now - _lastInvokeTime).TotalMilliseconds >= debounceMs)) return;
58-
59-
_lastInvokeTime = now;
60-
await InvokeCallbackAsync(_latestValue!, ct);
45+
// If we weren't cancelled, execute the callback
46+
if (!debounceToken.IsCancellationRequested) {
47+
await InvokeCallbackAsync(_latestValue!, ct);
48+
}
6149
}
6250
catch (OperationCanceledException) {
6351
// Ignore
@@ -70,6 +58,7 @@ protected async Task DebouncerLogicAsync(T? value = default, CancellationToken c
7058
}
7159
}
7260

61+
7362
public async ValueTask DisposeAsync() {
7463
if (_isDisposed) return;
7564

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using CodeOfChaos.Extensions.Debouncers;
5+
6+
// ReSharper disable once CheckNamespace
7+
namespace System;
8+
9+
// ---------------------------------------------------------------------------------------------------------------------
10+
// Code
11+
// ---------------------------------------------------------------------------------------------------------------------
12+
public static class DebouncerExtensions {
13+
public static Debouncer<T> GetDebouncer<T>(this Action<T> callback, int debounceMs) => Debouncer<T>.FromDelegate(callback, debounceMs);
14+
public static Debouncer GetDebouncer(this Action callback, int debounceMs) => Debouncer.FromDelegate(callback, debounceMs);
15+
16+
public static Debouncer<T> GetDebouncer<T>(this Func<T, Task> callback, int debounceMs) => Debouncer<T>.FromDelegate(callback, debounceMs);
17+
public static Debouncer GetDebouncer(this Func<Task> callback, int debounceMs) => Debouncer.FromDelegate(callback, debounceMs);
18+
public static Debouncer<T> GetDebouncer<T>(this Func<T, CancellationToken, Task> callback, int debounceMs) => Debouncer<T>.FromDelegate(callback, debounceMs);
19+
public static Debouncer GetDebouncer(this Func<CancellationToken, Task> callback, int debounceMs) => Debouncer.FromDelegate(callback, debounceMs);
20+
21+
}

0 commit comments

Comments
 (0)