# @coda/async

Async patterns for resilient, rate-aware applications. Provides composable building blocks for concurrency control, retry logic, circuit breaking, and request throttling.

## Installation

```jsonc
// package.json
{ "dependencies": { "@coda/async": "workspace:*" } }
```

## Core Utilities

```ts
import { sleep, raceAbort, raceWithTimeout, TimeoutError } from "@coda/async";

await sleep(100); // yield for 100 ms
await raceAbort(signal, longRunningTask); // cancel on abort
await raceWithTimeout(fetch(url), 5000); // throws TimeoutError after 5 s
```

| Function          | Description                                              |
| ----------------- | -------------------------------------------------------- |
| `sleep(ms)`       | Promise-based delay (optional `AbortSignal`)             |
| `yieldEvent()`    | Yield the event loop for one microtask                   |
| `raceAbort`       | Race a promise against an `AbortSignal`                  |
| `raceWithTimeout` | Race a promise against a timeout (throws `TimeoutError`) |
| `TimeoutError`    | Error subclass thrown on timeout                         |

## Polling & Intervals

```ts
import { interval, poll, startPolling } from "@coda/async";

// Async generator — yields a Tick on each interval
for await (const tick of interval(5000, { signal })) {
  await doWork();
  // tick.stop() to break early
}

// Poll a function, yielding { value } or { error }
for await (const result of poll(checkStatus, 3000)) {
  if (result.value?.ready) break;
}

// Fire-and-forget polling with jitter
const handle = startPolling(syncData, 60_000, 5000);
handle.stop(); // when done
```

| Function       | Description                                   |
| -------------- | --------------------------------------------- |
| `interval`     | Async generator yielding on a fixed interval  |
| `poll`         | Async generator polling a function repeatedly |
| `startPolling` | Fire-and-forget polling with optional jitter  |

## Concurrency

```ts
import { Semaphore, SingleFlight, LazyResource } from "@coda/async";

// Bound concurrency to 5
const sem = new Semaphore(5);
await sem.acquire();
try {
  await work();
} finally {
  sem.release();
}

// Deduplicate concurrent fetches for the same key
const sf = new SingleFlight<Response>();
const data = await sf.do("user:123", () => fetch("/api/users/123"));
```

| Class          | Description                                                     |
| -------------- | --------------------------------------------------------------- |
| `Semaphore`    | Counting semaphore for bounding concurrent async operations     |
| `SingleFlight` | Request coalescing — deduplicates concurrent calls for same key |
| `LazyResource` | Abstract base for expensive async init with coalesced loading   |

## Retry

```ts
import { retry, ExponentialStrategy, FixedStrategy } from "@coda/async";

const result = await retry(
  3,
  new ExponentialStrategy({ baseMs: 100, maxMs: 5000 }),
  async (attempt) => {
    const res = await fetch(url);
    if (res.ok) return { done: true, value: res };
    if (res.status >= 500) return { done: false }; // retryable
    return { done: true, value: res }; // non-retryable
  },
);
```

| Export                     | Description                                            |
| -------------------------- | ------------------------------------------------------ |
| `retry(max, strategy, fn)` | Retry a callback with pluggable back-off               |
| `ExponentialStrategy`      | Exponential backoff with jitter (100 ms base, 2 s cap) |
| `FixedStrategy`            | Constant delay between attempts                        |
| `DynamicStrategy`          | Caller-provided delay hint (e.g. `Retry-After` header) |
| `RetryStrategy`            | Interface: `delay(attempt, ms?) => number`             |
| `RetryResult<T>`           | `{ done: true, value: T } \| { done: false }`          |

## Circuit Breaker

```ts
import {
  CircuitBreaker,
  ConsecutiveFailureStrategy,
  CircuitOpenError,
} from "@coda/async";

const breaker = new CircuitBreaker({
  fn: callExternalService,
  strategy: new ConsecutiveFailureStrategy({ threshold: 5 }),
  cooldownMs: 30_000,
  successThreshold: 2,
  onStateChange: (state) => console.log("Circuit:", state),
});

try {
  const result = await breaker.execute(req);
} catch (e) {
  if (e instanceof CircuitOpenError) {
    console.log(`Retry after ${e.retryAfterMs} ms`);
  }
}
```

| Export                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `CircuitBreaker`             | State machine: closed → open → half-open                  |
| `CircuitBreakerStrategy`     | Interface: `failure() => boolean`, `success()`, `clear()` |
| `ConsecutiveFailureStrategy` | Trips after N consecutive failures                        |
| `FixedWindowStrategy`        | Trips after N failures within a fixed time window         |
| `RollingWindowStrategy`      | Trips after N failures within a rolling time window       |
| `CircuitOpenError`           | Thrown when circuit is open (has `retryAfterMs`)          |
| `CircuitState`               | `"closed" \| "open" \| "half-open"`                       |

## Throttling

```ts
import { TokenBucketThrottler, SlidingWindowThrottler } from "@coda/async";

// Token bucket: 10 requests/sec with burst capacity of 20
const throttler = new TokenBucketThrottler({ capacity: 20, refillRate: 10 });
await throttler.acquire(); // waits if rate exceeded
throttler.tryAcquire(); // returns false if rate exceeded (non-blocking)

// Sliding window: max 100 requests per minute
const limiter = new SlidingWindowThrottler({ duration: 60_000, limit: 100 });
```

| Export                   | Description                                         |
| ------------------------ | --------------------------------------------------- |
| `Throttler`              | Base class — `acquire()`, `tryAcquire()`, `clear()` |
| `ThrottlerStrategy`      | Interface: `tryAcquire() => TryAcquireResult`       |
| `TokenBucketThrottler`   | Steady rate with burst capacity                     |
| `LeakyBucketThrottler`   | Steady drain rate, overflow throttled               |
| `LinearThrottler`        | Fixed minimum delay between requests                |
| `SlidingWindowThrottler` | Max N requests within a sliding time window         |

Each throttler has a corresponding `*Strategy` class if you need to compose custom behaviour with the base `Throttler`.

## Validation

| Function            | Description                                 |
| ------------------- | ------------------------------------------- |
| `assertPositiveInt` | Assert `>= 1` integer (throws `RangeError`) |
| `assertPositive`    | Assert `> 0` number (throws `RangeError`)   |
