Skip to content

Commit 565d6f8

Browse files
committed
Refactor: Simplify debouncer logic and improve code reuse
Removed `DebouncerBaseGeneric`, applied a single generic base class abstraction, and unified debounce logic across derived classes. Updated implementations to use `DebouncerLogicAsync` for consistent behavior. Adjusted unit tests and extensions to align with the changes.
1 parent 9e6eb0e commit 565d6f8

File tree

8 files changed

+66
-142
lines changed

8 files changed

+66
-142
lines changed

src/CodeOfChaos.Extensions.AspNetCore.Components/EventCallbacks/EventCallbackDebouncer.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@ namespace Microsoft.AspNetCore.Components;
1010
// ---------------------------------------------------------------------------------------------------------------------
1111
public class EventCallbackDebouncer(EventCallback callback, int debounceMs = DebouncerBase.DefaultDebounceMs)
1212
: DebouncerBase(debounceMs) {
13-
protected async override ValueTask InvokeCallbackAsync(CancellationToken ct = default) => await callback.InvokeAsync();
13+
14+
protected async override ValueTask InvokeCallbackAsync(CancellationToken ct = default)
15+
=> await callback.InvokeAsync();
16+
17+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
18+
=> DebouncerLogicAsync(default, ct);
1419
}
1520

1621
public class EventCallbackDebouncer<T>(EventCallback<T> callback, int debounceMs = EventCallbackDebouncer<T>.DefaultDebounceMs)
1722
: DebouncerBase<T>(debounceMs) {
1823

19-
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default) {
20-
await callback.InvokeAsync(item);
21-
}
24+
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default)
25+
=> await callback.InvokeAsync(item);
26+
27+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
28+
=> DebouncerLogicAsync(default, ct);
29+
30+
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
31+
=> DebouncerLogicAsync(item, ct);
2232
}

src/CodeOfChaos.Extensions/Debouncers/ActionDebouncer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ protected override ValueTask InvokeCallbackAsync(CancellationToken ct = default)
1313
action.Invoke();
1414
return ValueTask.CompletedTask;
1515
}
16+
17+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
18+
=> DebouncerLogicAsync(default, ct);
1619
}
1720

1821
public class ActionDebouncer<T>(Action<T> action, int debounceMs = DebouncerBase<T>.DefaultDebounceMs) : DebouncerBase<T>(debounceMs) {
@@ -22,6 +25,9 @@ protected override ValueTask InvokeCallbackAsync(T item, CancellationToken ct =
2225
action.Invoke(item);
2326
return ValueTask.CompletedTask;
2427
}
28+
29+
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
30+
=> DebouncerLogicAsync(item, ct);
2531
}
2632

2733

src/CodeOfChaos.Extensions/Debouncers/DebouncerBase.cs

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,74 @@
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
44
namespace CodeOfChaos.Extensions.Debouncers;
5-
65
// ---------------------------------------------------------------------------------------------------------------------
76
// Code
87
// ---------------------------------------------------------------------------------------------------------------------
9-
public abstract class DebouncerBase(int debounceMs) : IAsyncDisposable {
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 {
1019
protected const int DefaultDebounceMs = 100;
1120

1221
private readonly SemaphoreSlim _semaphore = new(1, 1);
1322
private CancellationTokenSource? _cts;
1423
private Task? _debounceTask;
1524
private bool _isDisposed;
16-
25+
private T? _latestValue;
26+
private DateTime _lastInvokeTime = DateTime.MinValue;
27+
1728
// -----------------------------------------------------------------------------------------------------------------
1829
// Methods
1930
// -----------------------------------------------------------------------------------------------------------------
20-
protected abstract ValueTask InvokeCallbackAsync(CancellationToken ct = default);
31+
protected abstract ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default);
2132

22-
public async Task InvokeDebouncedAsync(CancellationToken ct = default) {
33+
protected async Task DebouncerLogicAsync(T? value = default, CancellationToken ct = default) {
2334
ObjectDisposedException.ThrowIf(_isDisposed, this);
2435

2536
await _semaphore.WaitAsync(ct);
2637
try {
27-
if (_cts is not null) await _cts.CancelAsync();
28-
_cts?.Dispose();
38+
_latestValue = value;
39+
40+
if (_cts is not null) {
41+
await _cts.CancelAsync();
42+
_cts.Dispose();
43+
}
2944

3045
_cts = new CancellationTokenSource();
3146
CancellationTokenSource? localCts = _cts;
3247
CancellationToken debounceToken = localCts.Token;
33-
48+
49+
DateTime now = DateTime.UtcNow;
50+
if (!((now - _lastInvokeTime).TotalMilliseconds >= debounceMs)) return;
51+
52+
_lastInvokeTime = now;
3453
_debounceTask = Task.Run(function: async () => {
3554
try {
3655
await Task.Delay(debounceMs, debounceToken);
3756
if (!debounceToken.IsCancellationRequested) {
38-
57+
3958
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
40-
await InvokeCallbackAsync(ct);
59+
await InvokeCallbackAsync(_latestValue!, ct);
4160
}
4261
}
4362
catch (OperationCanceledException) {
4463
// Ignore
4564
}
4665
}, debounceToken);
47-
}
48-
catch (Exception ex) {
49-
Console.WriteLine(ex);
66+
5067
}
5168
finally {
5269
_semaphore.Release();
5370
}
5471
}
5572

56-
// -----------------------------------------------------------------------------------------------------------------
57-
// Methods
58-
// -----------------------------------------------------------------------------------------------------------------
5973
public async ValueTask DisposeAsync() {
6074
if (_isDisposed) return;
6175

@@ -67,11 +81,11 @@ public async ValueTask DisposeAsync() {
6781
await _cts.CancelAsync();
6882
_cts.Dispose();
6983
}
70-
84+
7185
if (_debounceTask is not null) {
7286
try { await _debounceTask; }
7387
catch {
74-
/* Ignore */
88+
// Ignore
7589
}
7690
}
7791
}
@@ -81,4 +95,4 @@ public async ValueTask DisposeAsync() {
8195

8296
GC.SuppressFinalize(this);
8397
}
84-
}
98+
}

src/CodeOfChaos.Extensions/Debouncers/DebouncerBaseGeneric.cs

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

src/CodeOfChaos.Extensions/Debouncers/TaskFuncDebouncer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,37 @@ protected async override ValueTask InvokeCallbackAsync(CancellationToken ct = de
1111
if (ct.IsCancellationRequested) return;
1212
await func.Invoke();
1313
}
14+
15+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
16+
=> DebouncerLogicAsync(default, ct);
1417
}
1518

1619
public class TaskFuncCancellableDebouncer(Func<CancellationToken, Task> func, int debounceMs = DebouncerBase.DefaultDebounceMs) : DebouncerBase(debounceMs) {
1720
protected async override ValueTask InvokeCallbackAsync(CancellationToken ct = default) {
1821
if (ct.IsCancellationRequested) return;
1922
await func.Invoke(ct);
2023
}
24+
25+
public Task InvokeDebouncedAsync(CancellationToken ct = default)
26+
=> DebouncerLogicAsync(default, ct);
2127
}
2228

2329
public class TaskFuncDebouncer<T>(Func<T, Task> func, int debounceMs = DebouncerBase<T>.DefaultDebounceMs) : DebouncerBase<T>(debounceMs) {
2430
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default) {
2531
if (ct.IsCancellationRequested) return;
2632
await func.Invoke(item);
2733
}
34+
35+
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
36+
=> DebouncerLogicAsync(item, ct);
2837
}
2938

3039
public class TaskFuncCancellableDebouncer<T>(Func<T, CancellationToken, Task> func, int debounceMs = DebouncerBase<T>.DefaultDebounceMs) : DebouncerBase<T>(debounceMs) {
3140
protected async override ValueTask InvokeCallbackAsync(T item, CancellationToken ct = default) {
3241
if (ct.IsCancellationRequested) return;
3342
await func.Invoke(item, ct);
3443
}
44+
45+
public Task InvokeDebouncedAsync(T item, CancellationToken ct = default)
46+
=> DebouncerLogicAsync(item, ct);
3547
}

src/CodeOfChaos.Extensions/Debouncers/FuncExtensions.cs renamed to src/CodeOfChaos.Extensions/Debouncers/TaskFuncExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace CodeOfChaos.Extensions.Debouncers;
66
// ---------------------------------------------------------------------------------------------------------------------
77
// Code
88
// ---------------------------------------------------------------------------------------------------------------------
9-
public static class FuncExtensions {
9+
public static class TaskFuncExtensions {
1010
public static TaskFuncDebouncer<T> GetDebouncer<T>(this Func<T, Task> callback, int debounceMs) => new(callback, debounceMs);
1111
public static TaskFuncDebouncer GetDebouncer(this Func<Task> callback, int debounceMs) => new(callback, debounceMs);
1212
public static TaskFuncCancellableDebouncer<T> GetDebouncer<T>(this Func<T, CancellationToken, Task> callback, int debounceMs) => new(callback, debounceMs);

tests/Tests.CodeOfChaos.Extensions/Debouncers/ActionDebouncerGenericTests.cs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,6 @@ public async Task GenericDebouncer_ShouldPassValueToCallback() {
2727
await Assert.That(receivedValue).IsEqualTo("test");
2828
}
2929

30-
[Test]
31-
public async Task GenericDebouncer_DefaultValue_ShouldWork() {
32-
// Arrange
33-
string receivedValue = "initial";
34-
Action<string> callback = value => {
35-
receivedValue = value;
36-
};
37-
38-
// Act
39-
await using var debouncer = new ActionDebouncer<string>(callback);
40-
await debouncer.InvokeDebouncedAsync();
41-
await Task.Delay(150);
42-
43-
// Assert
44-
await Assert.That(receivedValue).IsNull();
45-
}
46-
4730
[Test]
4831
public async Task GenericDebouncer_MultipleValues_ShouldUseLastValue() {
4932
// Arrange

tests/Tests.CodeOfChaos.Extensions/Debouncers/FuncDebouncerGenericTests.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,6 @@ public async Task GenericDebouncer_ShouldPassValueToCallback() {
2828
await Assert.That(receivedValue).IsEqualTo("test");
2929
}
3030

31-
[Test]
32-
public async Task GenericDebouncer_DefaultValue_ShouldWork() {
33-
// Arrange
34-
string receivedValue = "initial";
35-
Func<string, Task> callback = value => {
36-
receivedValue = value;
37-
return Task.CompletedTask;
38-
};
39-
40-
// Act
41-
await using var debouncer = new TaskFuncDebouncer<string>(callback);
42-
await debouncer.InvokeDebouncedAsync();
43-
await Task.Delay(150);
44-
45-
// Assert
46-
await Assert.That(receivedValue).IsNull();
47-
}
48-
4931
[Test]
5032
public async Task GenericDebouncer_MultipleValues_ShouldUseLastValue() {
5133
// Arrange

0 commit comments

Comments
 (0)