# `batch.ts` ## Purpose Implements `BatchTransport` — the HTTP transport used by `AnalyticsTracker` for the steady-state, non-unload flush path. Every call posts a JSON-encoded `{ events }` body to the configured endpoint, wrapped in an `AbortController` whose timeout is armed via the injected `TimersLike`. The transport short-circuits on `4xx` responses (no retry), retries with exponential backoff on `5xx` / network errors, and translates any post-abort failure into a typed `AnalyticsBrowserError(TRANSPORT_TIMEOUT)` so callers can discriminate timeouts from other transport failures. ## Features - `AbortController` + `timeoutMs` on every fetch — guaranteed bounded latency. - `4xx` short-circuit — no retry storm on stable client errors. - Exponential backoff on `5xx` / network — `retryDelay * 2 ** attempt`. - Injectable `fetchFn` and `timers` — every browser-API touch is reachable from tests without DOM globals. - Idempotent `shutdown()` — future `send()` calls are no-ops returning `true`. ## Usage: Basic ```ts import { BatchTransport } from "@suaveplan/analytics-browser/transport"; const transport = new BatchTransport({ endpoint: "/api/analytics" }); await transport.send(events); ``` ## Usage: Advanced ```ts import { BatchTransport } from "@suaveplan/analytics-browser/transport"; const transport = new BatchTransport({ endpoint: "https://collector.example.com/v1/events", maxRetries: 5, retryDelay: 500, timeoutMs: 5_000, fetchFn: async (url, init) => { const req = new Request(url, init); return fetch(req, { credentials: "include" }); }, }); ``` ## API Reference - `class BatchTransport implements Transport` - `new BatchTransport(options)` — `options: BatchTransportOptions { endpoint, maxRetries?, retryDelay?, timeoutMs?, fetchFn?, timers? }`. Throws `AnalyticsBrowserError(INVALID_OPTIONS)` when `endpoint` is empty. - `send(events) → Promise` — `true` on success, `false` on `4xx` or exhausted retries. Throws `AnalyticsBrowserError(TRANSPORT_TIMEOUT)` when the fetch is aborted by the timeout. - `shutdown() → Promise` — disables further sends. - `isActive() → boolean`. ## Implementation Notes The timeout `setTimer` callback aborts the controller with an `AnalyticsBrowserError(TRANSPORT_TIMEOUT)` as the reason. If the host's `fetch` rejects with that exact error, the transport rethrows it as-is. If the fetch rejects with a different error after the signal has aborted (e.g. a `DOMException` synthesised by the runtime), the transport still maps the failure to `TRANSPORT_TIMEOUT` because the signal's aborted state is the authoritative reason. Otherwise the failure is treated as a generic retryable network error and the loop continues until `maxRetries` is exhausted.