npm package: @nhlinh123/single-flight
Description: A lightweight, zero-dependency TypeScript library implementing the **Single-Flight pattern** (in-flight deduplication). It prevents duplicate concurrent requests for the same operation by sharing a single execution.
Latest version: 1.0.0

# Single-Flight Library

A lightweight, zero-dependency TypeScript library implementing the **Single-Flight pattern** (in-flight deduplication). It prevents duplicate concurrent requests for the same operation by sharing a single execution.

Ideal for:
- Preventing duplicate API calls (e.g., token refresh, cache warming).
- Deduplicating expensive async operations.
- Optimizing high-concurrency applications.

## Features

- 🚀 **Universal Support**: Works with **Promises** and **RxJS Observables**.
- 🔑 **Key-based Deduplication**: Requests with the same key share the execution.
- 🧹 **Auto Cleanup**: Automatically removes operations from the registry when they complete or error.
- 🎨 **Decorators**: Easy-to-use TypeScript decorators for class methods.
- ⚛️ **Framework Ready**: Built-in adapters for **React** (Hooks) and **Angular** (Service).
- 🌲 **Tree-Shakeable**: Import only what you need.
- 📦 **Tiny**: Core logic is < 5KB.

## Installation

```bash
npm install @nhlinh123/single-flight rxjs
```
*(Note: `rxjs` is a peer dependency. `react` and `@angular/core` are optional peer dependencies if you use those specific adapters.)*

## Usage

### 1. Core Class Usage

The `SingleFlight` class is the heart of the library.

```typescript
import { SingleFlight } from '@nhlinh123/single-flight';

const sf = new SingleFlight();

// --- Promise Example ---
const fetchUser = (id: string) => fetch(`/api/users/${id}`).then(r => r.json());

// These two calls will result in ONLY ONE network request
const user1 = sf.executeAsync('user-123', () => fetchUser('123'));
const user2 = sf.executeAsync('user-123', () => fetchUser('123'));

await Promise.all([user1, user2]);

// --- Observable Example ---
import { HttpClient } from '@angular/common/http'; // or any RxJS source

// These two subscriptions share the same source execution
sf.execute('settings', () => http.get('/api/settings')).subscribe();
sf.execute('settings', () => http.get('/api/settings')).subscribe();
```

### 2. Functional Wrappers

For quick, one-off usage without instantiating a class.

```typescript
import { singleFlightAsync, singleFlight } from '@nhlinh123/single-flight';

// Promise
await singleFlightAsync('my-key', () => myAsyncOperation());

// Observable
singleFlight('my-key', () => myObservable$).subscribe();
```

### 3. Decorators

Automatically deduplicate method calls based on arguments.

```typescript
import { SingleFlightDecorator } from '@nhlinh123/single-flight';

class UserService {
  // Generates key "user-123" when calling getUser('123')
  @SingleFlightDecorator('user-{0}')
  async getUser(id: string) {
    console.log('Fetching user...');
    return fetch(`/users/${id}`).then(r => r.json());
  }
}

const service = new UserService();
// "Fetching user..." logs only once
service.getUser('1');
service.getUser('1');
```

### 4. React Hook

Use `useSingleFlight` to manage async state with deduplication in React components.

```tsx
import { useSingleFlight } from '@nhlinh123/single-flight';

const UserProfile = ({ userId }) => {
  const { data, loading, error, refetch } = useSingleFlight(
    `user-${userId}`, 
    () => fetchUser(userId)
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>User: {data.name}</div>;
};
```

### 5. Angular Service

Inject `SingleFlightService` to share a global deduplication registry across your app.

```typescript
import { Component, OnInit } from '@angular/core';
import { SingleFlightService } from '@nhlinh123/single-flight';

@Component({ ... })
export class MyComponent implements OnInit {
  constructor(private sf: SingleFlightService, private http: HttpClient) {}

  ngOnInit() {
    // Share this request with any other component requesting 'config'
    this.sf.execute('config', () => this.http.get('/api/config'))
      .subscribe(config => console.log(config));
  }
}
```

## API Reference

### `SingleFlight` Class

- **`executeAsync<T>(key: string, fn: () => Promise<T>): Promise<T>`**
  Executes a Promise-based operation. If an operation with `key` is already in progress, returns the existing Promise.

- **`execute<T>(key: string, fn: () => Observable<T>): Observable<T>`**
  Executes an Observable-based operation. Uses `shareReplay` internally to multicast the result to all subscribers.

- **`has(key: string): boolean`**
  Checks if an operation with the given `key` is currently in-flight.

## License

ISC