# 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()) {
      <div>Loading...</div>
    } @else if (data.error()) {
      <div>Error: {{ data.error() }}</div>
    } @else {
      <div>Data: {{ data.value()?.name }}</div>
    }
  `,
})
export class MyComponent {
  #myDataResource = inject(MyDataResource);

  itemId = input.required<number>();

  // 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<TResourceValue[]>;

// 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> | TParams` - The params to fetch (can be a signal or static value)

**Returns:** `BatchingResourceRef<TResourceValue>`

#### `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<TResourceValue | undefined>;  // The cached result
  hasValue: Signal<boolean>;                  // True if cached (even if empty)
  isLoading: Signal<boolean>;                 // True while loading
  error: Signal<unknown>;                     // Error if failed
  status: Signal<ResourceStatus>;             // '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!
