Skip to content

Commit 799d6e9

Browse files
committed
Feat: Add FlushAsync to debouncers and update related tests for consistency and validation
1 parent b0718af commit 799d6e9

File tree

10 files changed

+47
-3
lines changed

10 files changed

+47
-3
lines changed

src/CodeOfChaos.Extensions/Debouncers/IDebouncer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace CodeOfChaos.Extensions.Debouncers;
88
// ---------------------------------------------------------------------------------------------------------------------
99
public interface IDebouncerBase {
1010
bool IsEmpty { get; }
11+
Task FlushAsync(CancellationToken ct = default);
1112
}
1213

1314
public interface IDebouncer : IDebouncerBase {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace CodeOfChaos.Extensions.Debouncers;
88
// ---------------------------------------------------------------------------------------------------------------------
99
// Code
1010
// ---------------------------------------------------------------------------------------------------------------------
11-
public abstract class DebouncerBase<T> : IAsyncDisposable {
11+
public abstract class DebouncerBase<T> : IAsyncDisposable, IDebouncerBase {
1212
protected const int DefaultDebounceMs = 100;
1313
public required int DebounceMs { get; init; }
1414

@@ -71,6 +71,11 @@ private async Task ExecuteDebounceTaskAsync(T? capturedValue, CancellationToken
7171
}
7272
}
7373

74+
public Task FlushAsync(CancellationToken ct = default) {
75+
EnsureNotDisposed();
76+
return _debounceTask ?? Task.CompletedTask;
77+
}
78+
7479
private void EnsureNotDisposed() {
7580
if (!_isDisposed) return;
7681
throw new ObjectDisposedException(nameof(DebouncerBase<T>));

src/CodeOfChaos.Extensions/Debouncers/Throttled/ThrottledDebouncerBase.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace CodeOfChaos.Extensions.Debouncers;
77
// ---------------------------------------------------------------------------------------------------------------------
88
// Code
99
// ---------------------------------------------------------------------------------------------------------------------
10-
public abstract class ThrottledDebouncerBase<T> : IAsyncDisposable {
10+
public abstract class ThrottledDebouncerBase<T> : IAsyncDisposable, IDebouncerBase {
1111
protected const int DefaultDebounceMs = 100;
1212
protected const int DefaultThrottleMs = 100;
1313

@@ -97,6 +97,11 @@ private async Task HandleDebounceExecution(T? capturedValue, CancellationToken e
9797
_debounceTask = null;
9898

9999
}
100+
101+
public Task FlushAsync(CancellationToken ct = default) {
102+
EnsureNotDisposed();
103+
return _debounceTask ?? Task.CompletedTask;
104+
}
100105

101106
private void EnsureNotDisposed() {
102107
if (!_isDisposed) return;

tests/Tests.CodeOfChaos.Extensions.AspNetCore.Components/EventCallbacks/EventCallbackDebouncerGenericTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public async Task GenericDebouncer_ShouldPassValueToCallback() {
2323
await using var debouncer = EventCallbackDebouncer<string>.FromEventCallback(callback);
2424
await debouncer.InvokeDebouncedAsync("test");
2525
await Task.Delay(150);
26+
await debouncer.FlushAsync();
2627

2728
// Assert
2829
await Assert.That(receivedValue).IsEqualTo("test");
@@ -41,6 +42,7 @@ public async Task GenericDebouncer_DefaultValue_ShouldWork() {
4142
await using var debouncer = EventCallbackDebouncer<string>.FromEventCallback(callback);
4243
await debouncer.InvokeDebouncedAsync();
4344
await Task.Delay(150);
45+
await debouncer.FlushAsync();
4446

4547
// Assert
4648
await Assert.That(receivedValue).IsEqualTo("initial");
@@ -61,6 +63,7 @@ public async Task GenericDebouncer_MultipleValues_ShouldUseLastValue() {
6163
await debouncer.InvokeDebouncedAsync("second");
6264
await debouncer.InvokeDebouncedAsync("third");
6365
await Task.Delay(150);
66+
await debouncer.FlushAsync();
6467

6568
// Assert
6669
await Assert.That(receivedValue).IsEqualTo("third");
@@ -82,6 +85,7 @@ public async Task GenericDebouncer_ConcurrentValues_ShouldBeThreadSafe() {
8285

8386
await Task.WhenAll(tasks);
8487
await Task.Delay(150);
88+
await debouncer.FlushAsync();
8589

8690
// Assert
8791
await Assert.That(receivedValues).HasCount().EqualTo(1);

tests/Tests.CodeOfChaos.Extensions.AspNetCore.Components/EventCallbacks/EventCallbackDebouncerTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public async Task DefaultDebounceMs_ShouldBe100() {
4242
await using EventCallbackDebouncer debouncer = EventCallbackDebouncer.FromEventCallback(callback);
4343
await debouncer.InvokeDebouncedAsync();
4444
await Task.Delay(150);
45+
await debouncer.FlushAsync();
4546

4647
// Assert
4748
await Assert.That(callCount).IsEqualTo(1);
@@ -68,6 +69,7 @@ public async Task CustomDebounceMs_ShouldRespectSpecifiedTime() {
6869

6970
// Wait for the remaining time
7071
await Task.Delay(100);
72+
await debouncer.FlushAsync();
7173
await Assert.That(callCount).IsEqualTo(1);
7274
}
7375

@@ -86,6 +88,7 @@ public async Task MultipleInvocations_ShouldDebounce() {
8688
await debouncer.InvokeDebouncedAsync();
8789
await debouncer.InvokeDebouncedAsync();
8890
await Task.Delay(150);
91+
await debouncer.FlushAsync();
8992

9093
// Assert
9194
await Assert.That(callCount).IsEqualTo(1);
@@ -107,6 +110,7 @@ public async Task ConcurrentInvocations_ShouldBeThreadSafe() {
107110

108111
await Task.WhenAll(tasks);
109112
await Task.Delay(150);
113+
await debouncer.FlushAsync();
110114

111115
// Assert
112116
await Assert.That(callCount).IsEqualTo(1);
@@ -153,6 +157,7 @@ public async Task InvocationDuringDebounce_ShouldCancelPrevious() {
153157
await Task.Delay(50);// Wait half the debounced time
154158
await debouncer.InvokeDebouncedAsync();
155159
await Task.Delay(150);
160+
await debouncer.FlushAsync();
156161

157162
// Assert
158163
await Assert.That(executionTimes).HasCount().EqualToOne();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public async Task GenericDebouncer_ShouldPassValueToCallback() {
2222
await using Debouncer<string> debouncer = Debouncer<string>.FromDelegate(callback);
2323
await debouncer.InvokeDebouncedAsync("test");
2424
await Task.Delay(150);
25+
await debouncer.FlushAsync();
2526

2627
// Assert
2728
await Assert.That(receivedValue).IsEqualTo("test");
@@ -41,6 +42,7 @@ public async Task GenericDebouncer_MultipleValues_ShouldUseLastValue() {
4142
await debouncer.InvokeDebouncedAsync("second");
4243
await debouncer.InvokeDebouncedAsync("third");
4344
await Task.Delay(150);
45+
await debouncer.FlushAsync();
4446

4547
// Assert
4648
await Assert.That(receivedValue).IsEqualTo("third");
@@ -61,6 +63,7 @@ public async Task GenericDebouncer_ConcurrentValues_ShouldBeThreadSafe() {
6163

6264
await Task.WhenAll(tasks);
6365
await Task.Delay(150);
66+
await debouncer.FlushAsync();
6467

6568
// Assert
6669
await Assert.That(receivedValues).HasCount().EqualTo(1);

tests/Tests.CodeOfChaos.Extensions/Debouncers/Regular/ActionDebouncerTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public async Task DefaultDebounceMs_ShouldBe100() {
2424
await using Debouncer debouncer = Debouncer.FromDelegate(callback);
2525
await debouncer.InvokeDebouncedAsync();
2626
await Task.Delay(150);
27+
await debouncer.FlushAsync();
2728

2829
// Assert
2930
await Assert.That(callCount).IsEqualTo(1);
@@ -51,6 +52,7 @@ public async Task CustomDebounceMs_ShouldRespectSpecifiedTime() {
5152

5253
// Wait for the remaining time
5354
await Task.Delay(100);
55+
await debouncer.FlushAsync();
5456
await Assert.That(callCount).IsEqualTo(1);
5557
}
5658

@@ -70,6 +72,7 @@ public async Task MultipleInvocations_ShouldDebounce() {
7072
await debouncer.InvokeDebouncedAsync();
7173
await debouncer.InvokeDebouncedAsync();
7274
await Task.Delay(150);
75+
await debouncer.FlushAsync();
7376

7477
// Assert
7578
await Assert.That(callCount).IsEqualTo(1);
@@ -90,6 +93,7 @@ public async Task ConcurrentInvocations_ShouldBeThreadSafe() {
9093

9194
await Task.WhenAll(tasks);
9295
await Task.Delay(150);
96+
await debouncer.FlushAsync();
9397

9498
// Assert
9599
await Assert.That(callCount).IsEqualTo(1);
@@ -135,6 +139,7 @@ public async Task InvocationDuringDebounce_ShouldCancelPrevious() {
135139
await Task.Delay(50);// Wait half the debounced time
136140
await debouncer.InvokeDebouncedAsync();
137141
await Task.Delay(150);
142+
await debouncer.FlushAsync();
138143

139144
// Assert
140145
await Assert.That(executionTimes).HasCount().EqualToOne();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public async Task GenericDebouncer_ShouldPassValueToCallback() {
2323
await using Debouncer<string> debouncer = Debouncer<string>.FromDelegate(callback);
2424
await debouncer.InvokeDebouncedAsync("test");
2525
await Task.Delay(150);
26+
await debouncer.FlushAsync();
2627

2728
// Assert
2829
await Assert.That(receivedValue).IsEqualTo("test");
@@ -43,6 +44,7 @@ public async Task GenericDebouncer_MultipleValues_ShouldUseLastValue() {
4344
await debouncer.InvokeDebouncedAsync("second");
4445
await debouncer.InvokeDebouncedAsync("third");
4546
await Task.Delay(150);
47+
await debouncer.FlushAsync();
4648

4749
// Assert
4850
await Assert.That(receivedValue).IsEqualTo("third");
@@ -64,6 +66,7 @@ public async Task GenericDebouncer_ConcurrentValues_ShouldBeThreadSafe() {
6466

6567
await Task.WhenAll(tasks);
6668
await Task.Delay(150);
69+
await debouncer.FlushAsync();
6770

6871
// Assert
6972
await Assert.That(receivedValues).HasCount().EqualTo(1);

tests/Tests.CodeOfChaos.Extensions/Debouncers/Regular/FuncDebouncerTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public async Task DefaultDebounceMs_ShouldBe100() {
2323
await using Debouncer debouncer = Debouncer.FromDelegate(callback);
2424
await debouncer.InvokeDebouncedAsync();
2525
await Task.Delay(150);
26+
await debouncer.FlushAsync();
2627

2728
// Assert
2829
await Assert.That(callCount).IsEqualTo(1);
@@ -49,6 +50,7 @@ public async Task CustomDebounceMs_ShouldRespectSpecifiedTime() {
4950

5051
// Wait for the remaining time
5152
await Task.Delay(100);
53+
await debouncer.FlushAsync();
5254
await Assert.That(callCount).IsEqualTo(1);
5355
}
5456

@@ -67,6 +69,7 @@ public async Task MultipleInvocations_ShouldDebounce() {
6769
await debouncer.InvokeDebouncedAsync();
6870
await debouncer.InvokeDebouncedAsync();
6971
await Task.Delay(150);
72+
await debouncer.FlushAsync();
7073

7174
// Assert
7275
await Assert.That(callCount).IsEqualTo(1);
@@ -88,6 +91,7 @@ public async Task ConcurrentInvocations_ShouldBeThreadSafe() {
8891

8992
await Task.WhenAll(tasks);
9093
await Task.Delay(150);
94+
await debouncer.FlushAsync();
9195

9296
// Assert
9397
await Assert.That(callCount).IsEqualTo(1);
@@ -134,6 +138,7 @@ public async Task InvocationDuringDebounce_ShouldCancelPrevious() {
134138
await Task.Delay(50);// Wait half the debounced time
135139
await debouncer.InvokeDebouncedAsync();
136140
await Task.Delay(150);
141+
await debouncer.FlushAsync();
137142

138143
// Assert
139144
await Assert.That(executionTimes).HasCount().EqualToOne();

tests/Tests.CodeOfChaos.Extensions/Debouncers/Throttled/ThrottledDebouncerGenericTests.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public async Task ThrottledDebouncer_ShouldPassValueToCallback() {
2525
ThrottledDebouncer<string> debouncer = ThrottledDebouncer<string>.FromDelegate(callback, debounceMs: 100, throttleMs: 500);
2626
await debouncer.InvokeDebouncedAsync("test");
2727
await Task.Delay(150);
28+
await debouncer.FlushAsync();
2829

2930
// Assert
3031
await Assert.That(receivedValue).IsEqualTo("test");
@@ -47,6 +48,7 @@ public async Task ThrottledDebouncer_MultipleValues_ShouldUseLastValue() {
4748
await debouncer.InvokeDebouncedAsync("second");
4849
await debouncer.InvokeDebouncedAsync("third");
4950
await Task.Delay(150);
51+
await debouncer.FlushAsync();
5052

5153
// Assert
5254
await Assert.That(receivedValue).IsEqualTo("third");
@@ -75,6 +77,7 @@ public async Task ThrottledDebouncer_ThrottleBehavior_ShouldExecuteImmediatelyAf
7577
await Task.Delay(100); // This should trigger throttle behavior (immediate execution)
7678

7779
await Task.Delay(50); // Give time for execution
80+
await debouncer.FlushAsync();
7881

7982
// Assert
8083
await Assert.That(receivedValues).HasCount().EqualTo(2);
@@ -109,7 +112,7 @@ public async Task ThrottledDebouncer_ContinuousRequests_ShouldRespectThrottleInt
109112
}
110113

111114
await Task.Delay(1000); // Wait for final execution
112-
// await debouncer.FlushAsync();
115+
await debouncer.FlushAsync();
113116

114117

115118
// Assert
@@ -157,6 +160,7 @@ await Task.WhenAll(
157160
);
158161

159162
await Task.Delay(75); // Wait for final debounced execution
163+
await debouncer.FlushAsync();
160164

161165
// Assert
162166
await Assert.That(executionCount).IsGreaterThanOrEqualTo(2);
@@ -183,6 +187,7 @@ public async Task ThrottledDebouncer_ConcurrentValues_ShouldBeThreadSafe() {
183187

184188
await Task.WhenAll(tasks);
185189
await Task.Delay(150);
190+
await debouncer.FlushAsync();
186191

187192
// Assert - May have 1 or 2 executions depending on timing (debounce + possible throttle)
188193
await Assert.That(receivedValues.Count).IsGreaterThanOrEqualTo(1);
@@ -232,6 +237,7 @@ public async Task ThrottledDebouncer_DefaultValues_ShouldUseDefaultDebounceAndTh
232237
DateTime startTime = DateTime.UtcNow;
233238
await debouncer.InvokeDebouncedAsync("test");
234239
await Task.Delay(150); // Should execute after default debounce (100ms)
240+
await debouncer.FlushAsync();
235241

236242
// Assert
237243
await Assert.That(executionTimes).HasCount().EqualTo(1);
@@ -258,6 +264,7 @@ public async Task ThrottledDebouncer_ZeroDebounce_ShouldExecuteImmediately() {
258264
DateTime startTime = DateTime.UtcNow;
259265
await debouncer.InvokeDebouncedAsync("immediate");
260266
await Task.Delay(50); // Give time for execution
267+
await debouncer.FlushAsync();
261268

262269
// Assert
263270
await Assert.That(receivedValue).IsEqualTo("immediate");
@@ -300,6 +307,7 @@ public async Task ThrottledDebouncer_LongRunningCallback_ShouldNotBlockSubsequen
300307
await Task.Delay(100); // Wait for second call
301308

302309
await Task.Delay(200); // Wait for both calls to complete
310+
await debouncer.FlushAsync();
303311

304312
// Assert
305313
await Assert.That(executionCount).IsEqualTo(2);

0 commit comments

Comments
 (0)