# BatchingResource - Reusable Batching Pattern ## Overview `BatchingResource` is a generic base class for creating smart batching resources that optimize multiple API requests by collecting params from multiple components and making a single batched request. ## Location ``` libs/common/data-access/src/lib/resources/batching-resource.base.ts ``` ## Features - ✅ **Automatic request batching** with configurable window - ✅ **Smart caching** (including empty results to prevent infinite loops) - ✅ **Reference counting** for automatic cleanup - ✅ **Per-item loading/error status** tracking - ✅ **Custom cache keys** support - ✅ **TypeScript generics** for type safety ## How to Use ### 1. Create a Resource Class Extend `BatchingResource` and implement the required abstract methods: ```typescript import { Injectable, inject } from '@angular/core'; import { BatchingResource } from '@isa/common/data-access'; @Injectable({ providedIn: 'root' }) export class MyDataResource extends BatchingResource< number, // TParams: What users pass to resource() MyApiParams, // TResourceParams: The type of batch API parameters MyDataType // TResourceValue: The type of results > { #myService = inject(MyService); constructor() { super(100); // batchWindowMs (optional, defaults to 100ms) } // Required: Fetch data from your API protected fetchFn(params: MyApiParams, signal: AbortSignal) { return this.#myService.fetchData(params, signal); } // Required: Convert params list to API request format protected buildParams(ids: number[]): MyApiParams { return { ids }; } // Required: Extract params from result for cache matching protected getKeyFromResult(result: MyDataType): number | undefined { return result.id; } // Required: Generate unique cache key from params protected getCacheKey(id: number): string { return String(id); } // Required: Extract params from resource params for status tracking protected extractKeysFromParams(params: MyApiParams): number[] { return params.ids; } } ``` ### 2. Use in Components ```typescript import { Component, computed, inject, input } from '@angular/core'; @Component({ selector: 'my-component', template: ` @if (data.isLoading()) {
Loading...
} @else if (data.error()) {
Error: {{ data.error() }}
} @else {
Data: {{ data.value()?.name }}
} `, }) export class MyComponent { #myDataResource = inject(MyDataResource); itemId = input.required(); // Automatically batches with other components data = this.#myDataResource.resource(this.itemId); // Use computed for derived values name = computed(() => this.data.value()?.name ?? 'Unknown'); } ``` ## API Reference ### Constructor ```typescript constructor(batchWindowMs?: number) // default: 100 ``` ### Abstract Methods (Must Implement) ```typescript // Fetch data from your API protected abstract fetchFn( params: TResourceParams, abortSignal: AbortSignal ): Promise; // Convert params list to API request format protected abstract buildParams(params: TParams[]): TResourceParams; // Extract params from result for cache matching protected abstract getKeyFromResult(result: TResourceValue): TParams | undefined; // Generate unique cache key from params protected abstract getCacheKey(params: TParams): string; // Extract params from resource params for status tracking protected abstract extractKeysFromParams(params: TResourceParams): TParams[]; ``` ### Methods #### `resource(params)` Returns a `BatchingResourceRef` for specific params. **Parameters:** - `params: Signal | TParams` - The params to fetch (can be a signal or static value) **Returns:** `BatchingResourceRef` #### `reload()` Reloads all currently tracked items by clearing cache. #### `reloadKeys(params: TParams[])` Reloads specific items by clearing their cache entries. ### BatchingResourceRef The object returned by `resource()`: ```typescript { value: Signal; // The cached result hasValue: Signal; // True if cached (even if empty) isLoading: Signal; // True while loading error: Signal; // Error if failed status: Signal; // 'idle' | 'loading' | 'resolved' | 'error' reload: () => void; // Reload this specific item } ``` ## Advanced: Custom Cache Keys For complex scenarios (like stock info with different warehouse IDs), encode all context in `TParams`: ```typescript // Define TParams as an object containing all necessary context export class StockResource extends BatchingResource< { itemId: number; warehouseId: string }, // TParams includes all context FetchStockParams, // TResourceParams StockInfo // TResourceValue > { #stockService = inject(StockService); constructor() { super(250); } protected fetchFn(params: FetchStockParams, signal: AbortSignal) { return this.#stockService.fetchStockInfos(params, signal); } protected buildParams( paramsList: { itemId: number; warehouseId: string }[] ): FetchStockParams { return { itemIds: paramsList.map(p => p.itemId), warehouseId: paramsList[0].warehouseId, // All should have same warehouse }; } protected getKeyFromResult(stock: StockInfo) { return { itemId: stock.itemId, warehouseId: stock.warehouseId }; } protected getCacheKey(params: { itemId: number; warehouseId: string }): string { return `${params.warehouseId}-${params.itemId}`; } protected extractKeysFromParams(params: FetchStockParams) { return params.itemIds.map(itemId => ({ itemId, warehouseId: params.warehouseId })); } } // Then use the complete params object const stockInfo = stockResource.resource({ itemId: 123, warehouseId: 'WH1' }); ``` ## How It Works ### Batching Process 1. **Component mounts** → Calls `resource(params)` 2. **Effect registers params** → Adds to pending batch queue 3. **Batch window expires** (default 100ms) → Pending params move to committed 4. **Resource triggers** → Fetches data for all committed params 5. **Results cached** → Both successful and empty results stored 6. **Component unmounts** → Reference count decremented, cleanup if zero ### Infinite Loop Prevention The key innovation is **caching empty results**: ```typescript // If API returns no data for params, we still cache it with undefined if (!hasResult) { cache.set(cacheKey, undefined); } ``` This prevents the resource from thinking the params are "uncached" and re-requesting them infinitely. ## Example: Stock Info Resource (Real-World) ```typescript @Injectable({ providedIn: 'root' }) export class StockInfoResource extends BatchingResource< number, // TParams - Item IDs FetchStockParams, // TResourceParams - { itemIds: number[] } StockInfo // TResourceValue - { itemId: number, inStock: number, ... } > { #stockService = inject(RemissionStockService); constructor() { super(250); // Wait longer for more batching } protected fetchFn(params: FetchStockParams, signal: AbortSignal) { return this.#stockService.fetchStockInfos(params, signal); } protected buildParams(itemIds: number[]): FetchStockParams { return { itemIds }; } protected getKeyFromResult(stock: StockInfo): number | undefined { return stock.itemId; } protected getCacheKey(itemId: number): string { return String(itemId); } protected extractKeysFromParams(params: FetchStockParams): number[] { return params.itemIds; } } // Usage in component const stockInfo = inject(StockInfoResource).resource(itemId); const inStock = computed(() => stockInfo.value()?.inStock ?? 0); ``` ## Benefits Over Manual Implementation ✅ **~500 lines** of complex batching logic → **~20 lines** of config ✅ **Automatic** reference counting and cleanup ✅ **Infinite loop prevention** built-in ✅ **Type-safe** with TypeScript generics ✅ **Reusable** across any domain (stock, pricing, availability, etc.) ✅ **Tested** and battle-proven pattern ## Migration Guide If you have an existing custom resource, extract these parts: 1. **Params type** (TParams): What do users pass to identify each item? (e.g., `number` for item IDs) 2. **Resource params type** (TResourceParams): What does your batch API accept? (e.g., `{ itemIds: number[] }`) 3. **Result type** (TResourceValue): What does your API return per item? (e.g., `StockInfo`) 4. **Service injection**: Your API service (e.g., `inject(MyService)`) 5. **Fetch method**: Your service method that calls the API 6. **Build params logic**: How to convert params list → resource params 7. **Get params from result logic**: How to extract the params from each result 8. **Cache key logic**: How to generate a unique string key from params 9. **Extract params logic**: How to get params list back from resource params (for status tracking) Then implement these as abstract methods in your `BatchingResource` subclass!