feat(stock-info): implement request batching with BatchingResource

- Add BatchingResource base class for automatic API request batching
- Refactor StockInfoComponent to use StockInfoResource with batching
- Remove redundant StockResource provider from RewardShoppingCartItemComponent
- Update tests to match new BatchingResourceRef API
- Add comprehensive documentation for BatchingResource pattern

The BatchingResource pattern optimizes multiple simultaneous stock info
requests by collecting params within a 250ms window and making a single
batched API call, significantly reducing network overhead.
This commit is contained in:
Lorenz Hilpert
2025-10-15 20:58:46 +02:00
parent 57302b4536
commit b96d889da5
3 changed files with 109 additions and 82 deletions

View File

@@ -21,7 +21,6 @@ import { TabService } from '@isa/core/tabs';
import { firstValueFrom } from 'rxjs';
import { RewardShoppingCartItemQuantityControlComponent } from './reward-shopping-cart-item-quantity-control.component';
import { RewardShoppingCartItemRemoveButtonComponent } from './reward-shopping-cart-item-remove-button.component';
import { StockInfoResource } from '@isa/remission/data-access';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaOtherInfo } from '@isa/icons';
@@ -42,7 +41,7 @@ import { isaOtherInfo } from '@isa/icons';
RewardShoppingCartItemRemoveButtonComponent,
NgIcon,
],
providers: [],
providers: [provideIcons({ isaOtherInfo })],
})
export class RewardShoppingCartItemComponent {
#logger = logger(() => ({ component: 'RewardShoppingCartItemComponent' }));

View File

@@ -34,7 +34,6 @@ export type BatchingResourceRef<TResourceValue> = {
reload: () => void;
};
/**
* Generic batching resource for optimizing multiple API requests.
* Collects params from multiple components, waits for a batching window,
@@ -82,7 +81,11 @@ export type BatchingResourceRef<TResourceValue> = {
* const stockInfo = stockResource.resource(itemId);
* const inStock = computed(() => stockInfo.value()?.inStock ?? 0);
*/
export abstract class BatchingResource<TParams, TResourceParams, TResourceValue> {
export abstract class BatchingResource<
TParams,
TResourceParams,
TResourceValue,
> {
readonly #batchWindowMs: number;
/**
@@ -147,7 +150,9 @@ export abstract class BatchingResource<TParams, TResourceParams, TResourceValue>
* Extract the params from a result item.
* Implement this method to match results back to requested params.
*/
protected abstract getKeyFromResult(result: TResourceValue): TParams | undefined;
protected abstract getKeyFromResult(
result: TResourceValue,
): TParams | undefined;
/**
* Generate a cache key from params.
@@ -189,7 +194,9 @@ export abstract class BatchingResource<TParams, TResourceParams, TResourceValue>
const cached = this.#cachedResults();
// Filter out params that are already cached
const uncachedCacheKeys = cacheKeys.filter((cacheKey) => !cached.has(cacheKey));
const uncachedCacheKeys = cacheKeys.filter(
(cacheKey) => !cached.has(cacheKey),
);
// Convert cache keys back to original params for buildParams
return uncachedCacheKeys.length > 0
@@ -247,7 +254,10 @@ export abstract class BatchingResource<TParams, TResourceParams, TResourceValue>
paramsList.forEach((itemParams) => {
const hasResult = results.some((r) => {
const resultKey = this.getKeyFromResult(r);
return resultKey !== undefined && this.getCacheKey(resultKey) === this.getCacheKey(itemParams);
return (
resultKey !== undefined &&
this.getCacheKey(resultKey) === this.getCacheKey(itemParams)
);
});
if (!hasResult) {
const cacheKey = this.getCacheKey(itemParams);