# @supercat1337/fetcher

[![npm version](https://badge.fury.io/js/%40supercat1337%2Ffetcher.svg)](https://www.npmjs.com/package/@supercat1337/fetcher)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Advanced fetch utility with cancellation, smart retries, singleton requests, XHR progress, and full TypeScript support.**

---

## Features

- ✅ **Cancellation** – abort ongoing requests at any level using standard `AbortController`.
- ✅ **Smart retries** – retry only on temporary failures (network errors, 5xx). Customizable via `shouldRetry`.
- ✅ **Singleton fetcher** – automatically cancel previous request when a new one starts.
- ✅ **XHR with progress** – upload/download progress, timeouts, and retries.
- ✅ **Fetcher manager** – create multiple retry+singleton fetchers, cancel all at once.
- ✅ **Memory safe** – automatic cleanup via `AbortController`, no listeners to leak.
- ✅ **Full TypeScript** – via JSDoc, no compilation needed.
- ✅ **Zero dependencies** – uses only native browser/Node.js APIs.

---

## Installation

```bash
npm install @supercat1337/fetcher
```

---

## Quick Start

### Basic retry fetch (native)

```javascript
import { createRetryFetch } from '@supercat1337/fetcher';

const fetchWithRetry = createRetryFetch({ maxAttempts: 3, waitTime: 1000 });

try {
    const response = await fetchWithRetry('https://api.example.com/data');
    const data = await response.json();
} catch (err) {
    console.error('Failed after 3 attempts', err);
}
```

### Singleton fetch (auto‑cancel previous)

```javascript
import { createSingletonFetch } from '@supercat1337/fetcher';

const fetchSingleton = createSingletonFetch();
fetchSingleton('/api/search?q=hello');
fetchSingleton('/api/search?q=world'); // cancels the first one
```

### XHR with upload progress and retries

```javascript
import { createRetryXhr } from '@supercat1337/fetcher';

const upload = createRetryXhr({ maxAttempts: 3, waitTime: 2000 });

const response = await upload('https://api.example.com/upload', {
    method: 'POST',
    body: fileData,
    onUploadProgress: (loaded, total) => console.log(`${loaded}/${total}`),
    onProgress: (loaded, total) => console.log(`Download: ${loaded}/${total}`),
});
```

### Full‑featured Fetcher manager with XHR support

```javascript
import { Fetcher } from '@supercat1337/fetcher';

const fetcher = new Fetcher();

const { fetch: fetchUsers } = fetcher.createFetchFunction({ maxAttempts: 2 });
const { fetch: uploadFile } = fetcher.createXhrFetchFunction({ maxAttempts: 3 });

// Cancel everything at once
document.getElementById('cancelBtn').onclick = () => fetcher.cancel();

// Clean up when done
fetcher.destroy();
```

### Composing retry and singleton manually

```javascript
import { withRetry, SingletonFetcher, xhrFetch } from '@supercat1337/fetcher';

const retryXhr = withRetry(xhrFetch, { maxAttempts: 3 });
const singleton = new SingletonFetcher(retryXhr);
const response = await singleton.fetch('/data', { onProgress: p => console.log(p) });
```

---

## API Reference

### `withRetry(fetcher, options)`

| Option        | Type                                    | Default   | Description                                                 |
| ------------- | --------------------------------------- | --------- | ----------------------------------------------------------- |
| `maxAttempts` | `number`                                | `3`       | Total attempts (including first)                            |
| `waitTime`    | `number`                                | `1000`    | Delay between retries (ms)                                  |
| `shouldRetry` | `(error: Error \| Response) => boolean` | see below | Custom predicate. Default: retry on network errors and 5xx. |

Returns a function with the same signature as `fetcher`.

### `createRetryFetch(options?)`

Same as `withRetry(fetch, options)`.

### `createRetryXhr(options?)`

Same as `withRetry(xhrFetch, options)`.

### `createSingletonFetch(customFetch?)`

Returns a singleton fetch function using native `fetch` (or custom fetch).

### `createSingletonXhr()`

Returns a singleton fetch function using `xhrFetch` (progress supported).

### `createSingletonRetryXhr(retryOptions?)`

Returns a singleton + retry function using `xhrFetch`.

### `class SingletonFetcher`

- `constructor(customFetch?)`
- `fetch(resource, options): Promise<Response>`
- `cancel(): void`
- `cancelAndWait(): Promise<void>`
- `isLoading: boolean` (getter)

### `class Fetcher`

- `createFetchFunction(options?)` → `{ fetch, cancel, cancelAndWait }` (native fetch)
- `createXhrFetchFunction(options?)` → `{ fetch, cancel, cancelAndWait }` (XHR with progress)
- `fetch(resource, options?)` – one‑off cancellable fetch (native)
- `cancel()` – cancels all ongoing fetches created by this instance
- `destroy()` – aborts all requests and cleans up

---

## Advanced Usage

### Custom retry predicate

```javascript
const fetchWithRetry = createRetryFetch({
    maxAttempts: 3,
    shouldRetry: err => {
        if (err instanceof Response) return err.status === 429; // rate limit
        return err.code === 'ECONNRESET';
    },
});
```

### Using with AbortController

```javascript
const controller = new AbortController();
const fetchSingleton = createSingletonFetch();
const promise = fetchSingleton('/api/long-task', { signal: controller.signal });
controller.abort(); // cancels the request
```

### Memory cleanup

```javascript
const fetcher = new Fetcher();
const { fetch } = fetcher.createFetchFunction();

// When you no longer need this fetcher:
fetcher.destroy(); // aborts all ongoing requests and releases resources
```

---

## TypeScript support

The package is fully typed via JSDoc. In a TypeScript project, you get autocompletion and type checking without extra configuration.

```typescript
import { createRetryXhr, type XhrFetchOptions } from '@supercat1337/fetcher';

const fetch = createRetryXhr({ maxAttempts: 3 });
const response = await fetch('/api', {
    onProgress: (loaded, total) => console.log(loaded, total),
} as XhrFetchOptions);
```

---

## Error handling

- Network errors and failed responses are caught and passed to `shouldRetry`.
- If all retries fail, the original error is thrown.
- Cancellation throws a `DOMException` with `name = "AbortError"`.

---

## Browser / Node support

| Environment     | Support                                              |
| --------------- | ---------------------------------------------------- |
| Modern browsers | ✅ (Chrome 85+, Firefox 88+, Safari 15.4+, Edge 85+) |
| Node.js         | ✅ 18+ (native fetch + AbortSignal.any)              |

> No polyfills are included. For older environments, provide your own `AbortSignal.any` polyfill.

---

## License

MIT © [Albert Bazaleev aka supercat1337](https://github.com/supercat1337)
