Skip to content

Commit e723d43

Browse files
committed
feat: add ExecuteCachedAsync for deduplicated async actions with caching
- Introduced ExecuteCachedAsync method in AsyncActionExecutor to handle deduplication of async fetch and state updates. - Updated IAsyncActionExecutor interface to include new caching methods. - Enhanced StoreComponentWithUtilities to support ExecuteCachedAsync. - Added comprehensive unit tests for ExecuteCachedAsync covering various scenarios including concurrent calls, caching behavior, and error handling. - Updated documentation and navigation links in async helpers to include ExecuteCachedAsync.
1 parent b25a144 commit e723d43

13 files changed

Lines changed: 1346 additions & 1 deletion

File tree

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,38 @@ async Task LoadUserDetails(int userId)
385385
}
386386
```
387387

388+
### 6. ExecuteCachedAsync - Deduplicated State Updates
389+
390+
Combines caching with automatic state management. Unlike `ExecuteAsync` + `LazyLoad`, this deduplicates **both** the fetch **and** the state updates:
391+
392+
```csharp
393+
// Multiple components calling this concurrently:
394+
// - Only ONE loading state update
395+
// - Only ONE async fetch
396+
// - Only ONE success/error state update
397+
// Result: 2 state updates instead of 2×N
398+
async Task LoadProduct(int productId, CancellationToken ct = default)
399+
{
400+
var product = await ExecuteCachedAsync(
401+
$"product-{productId}",
402+
async () => await ProductService.GetAsync(productId),
403+
loading: s => s with { Product = s.Product.ToLoading() },
404+
success: (s, product) => s with { Product = AsyncData.Success(product) },
405+
error: (s, ex) => s with { Product = AsyncData.Failure(ex.Message) },
406+
cacheFor: TimeSpan.FromMinutes(5),
407+
cancellationToken: ct // Optional cancellation support
408+
);
409+
}
410+
```
411+
412+
**When to use which helper:**
413+
414+
| Scenario | Method |
415+
|----------|--------|
416+
| Multiple components load same data | `ExecuteCachedAsync` |
417+
| Single component, no deduplication needed | `ExecuteAsync` |
418+
| Need data without state updates | `LazyLoad` |
419+
388420
---
389421

390422
## Optimistic Updates
@@ -1078,6 +1110,7 @@ protected Task UpdateDebounced(Func<TState, TState> updater, int delayMs);
10781110
protected Task UpdateThrottled(Func<TState, TState> updater, int intervalMs);
10791111
protected Task ExecuteAsync<T>(Func<Task<T>> action, ...);
10801112
protected Task<T> LazyLoad<T>(string key, Func<Task<T>> loader, TimeSpan? cacheFor);
1113+
protected Task<T> ExecuteCachedAsync<T>(string key, Func<Task<T>> action, ..., TimeSpan? cacheFor, CancellationToken ct = default);
10811114
```
10821115

10831116
### Registration

docs-site/api-reference.html

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<li><a href="./async-helpers/execute-async.html" class="nav-link"><span class="nav-icon"></span><span>ExecuteAsync</span></a></li>
6565
<li><a href="./async-helpers/update-throttled.html" class="nav-link"><span class="nav-icon">💧</span><span>UpdateThrottled</span></a></li>
6666
<li><a href="./async-helpers/lazy-load.html" class="nav-link"><span class="nav-icon">📥</span><span>LazyLoad</span></a></li>
67+
<li><a href="./async-helpers/execute-cached-async.html" class="nav-link"><span class="nav-icon">🔗</span><span>ExecuteCachedAsync</span></a></li>
6768
</ul>
6869
</div>
6970

@@ -145,6 +146,14 @@ <h2 id="store-component">StoreComponent&lt;TState&gt;</h2>
145146
Func&lt;TState, T, TState&gt; success,
146147
Func&lt;TState, Exception, TState&gt;? error = null);
147148
protected Task&lt;T&gt; LazyLoad&lt;T&gt;(string key, Func&lt;Task&lt;T&gt;&gt; loader, TimeSpan? cacheFor = null);
149+
protected Task&lt;T&gt; ExecuteCachedAsync&lt;T&gt;(
150+
string key,
151+
Func&lt;Task&lt;T&gt;&gt; action,
152+
Func&lt;TState, TState&gt; loading,
153+
Func&lt;TState, T, TState&gt; success,
154+
Func&lt;TState, Exception, TState&gt;? error = null,
155+
TimeSpan? cacheFor = null,
156+
CancellationToken ct = default);
148157
}</code></pre>
149158
</div>
150159

@@ -270,6 +279,43 @@ <h4><code>LazyLoad</code></h4>
270279
$"user-{userId}",
271280
() => UserService.GetUserAsync(userId),
272281
TimeSpan.FromMinutes(5)
282+
);</code></pre>
283+
</div>
284+
</div>
285+
286+
<div class="api-section">
287+
<h4><code>ExecuteCachedAsync</code></h4>
288+
<p>Executes an async operation with full deduplication of both the fetch AND the state updates. When multiple components call this with the same key, only 2 state updates occur instead of N×2.</p>
289+
<div class="code-block">
290+
<pre><code class="language-csharp">protected Task&lt;T&gt; ExecuteCachedAsync&lt;T&gt;(
291+
string key,
292+
Func&lt;Task&lt;T&gt;&gt; action,
293+
Func&lt;TState, TState&gt; loading,
294+
Func&lt;TState, T, TState&gt; success,
295+
Func&lt;TState, Exception, TState&gt;? error = null,
296+
TimeSpan? cacheFor = null,
297+
CancellationToken ct = default)</code></pre>
298+
</div>
299+
<p><strong>Parameters:</strong></p>
300+
<ul>
301+
<li><code>key</code> - Unique cache key for deduplication</li>
302+
<li><code>action</code> - Async operation to execute</li>
303+
<li><code>loading</code> - State transformation for loading state</li>
304+
<li><code>success</code> - State transformation for success with result</li>
305+
<li><code>error</code> - Optional state transformation for error handling</li>
306+
<li><code>cacheFor</code> - Optional cache duration</li>
307+
<li><code>ct</code> - Optional cancellation token to cancel the operation</li>
308+
</ul>
309+
<p><strong>Example:</strong></p>
310+
<div class="code-block">
311+
<pre><code class="language-csharp">// Multiple components calling this = only 2 state updates
312+
var product = await ExecuteCachedAsync(
313+
$"product-{productId}",
314+
async () => await ProductService.GetAsync(productId),
315+
loading: s => s with { Product = s.Product.ToLoading() },
316+
success: (s, p) => s with { Product = AsyncData.Success(p) },
317+
error: (s, ex) => s with { Product = AsyncData.Failure(ex.Message) },
318+
cacheFor: TimeSpan.FromMinutes(5)
273319
);</code></pre>
274320
</div>
275321
</div>
@@ -889,6 +935,10 @@ <h2 id="quick-reference">Quick Reference</h2>
889935
<td><code>LazyLoad()</code></td>
890936
<td>Caching with request deduplication</td>
891937
</tr>
938+
<tr>
939+
<td><code>ExecuteCachedAsync()</code></td>
940+
<td>Full deduplication of fetch and state updates</td>
941+
</tr>
892942
<tr>
893943
<td><code>AsyncData&lt;T&gt;</code></td>
894944
<td>Async state representation</td>

docs-site/async-helpers/async-data.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<li><a href="../async-helpers/execute-async.html" class="nav-link"><span class="nav-icon"></span><span>ExecuteAsync</span></a></li>
6565
<li><a href="../async-helpers/update-throttled.html" class="nav-link"><span class="nav-icon">💧</span><span>UpdateThrottled</span></a></li>
6666
<li><a href="../async-helpers/lazy-load.html" class="nav-link"><span class="nav-icon">📥</span><span>LazyLoad</span></a></li>
67+
<li><a href="../async-helpers/execute-cached-async.html" class="nav-link"><span class="nav-icon">🔗</span><span>ExecuteCachedAsync</span></a></li>
6768
</ul>
6869
</div>
6970

docs-site/async-helpers/execute-async.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<li><a href="../async-helpers/execute-async.html" class="nav-link"><span class="nav-icon"></span><span>ExecuteAsync</span></a></li>
6565
<li><a href="../async-helpers/update-throttled.html" class="nav-link"><span class="nav-icon">💧</span><span>UpdateThrottled</span></a></li>
6666
<li><a href="../async-helpers/lazy-load.html" class="nav-link"><span class="nav-icon">📥</span><span>LazyLoad</span></a></li>
67+
<li><a href="../async-helpers/execute-cached-async.html" class="nav-link"><span class="nav-icon">🔗</span><span>ExecuteCachedAsync</span></a></li>
6768
</ul>
6869
</div>
6970

0 commit comments

Comments
 (0)