import { HttpError } from "./HttpError.mjs"; //#region ../http/src/Http.types.d.ts type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; type HttpData = string | object | FormData | (typeof globalThis extends { HTMLFormElement: infer T; } ? T : never); type HttpParams = Record; /** * Minimal interface any cache driver must satisfy. * Compatible with @mongez/cache drivers out of the box. */ interface CacheDriver { get(key: string): Promise; set(key: string, value: unknown, ttl?: number): Promise | void; remove?(key: string): Promise | void; /** Clear all entries. Required for `http.invalidateAll()`. */ clear?(): Promise | void; } interface HttpCacheConfig { driver: CacheDriver; /** Time-to-live in seconds. Default: 300 (5 min). */ ttl?: number; /** * Custom key generator. Default: baseURL + path + serialised params. * * **Server-side:** the default key is URL + params only, so it does not distinguish * one caller from another. Outside a browser the cache therefore refuses to run * unless you supply either this generator or a per-request `cacheKey` — see * {@link HttpConfig.cache}. Close over the identity that matters in your app: * * @example * // Request-scoped instance — the user id is baked into every key. * const http = new Http({ * baseURL, * cache: { driver, generateKey: (url, params) => * `u:${session.userId}:${url}:${JSON.stringify(params ?? {})}` }, * }); */ generateKey?: (url: string, params?: HttpParams) => string; } interface HttpRetryConfig { /** Number of retry attempts. */ attempts: number; /** Base delay between retries in ms. */ delay: number; /** Double delay after each attempt. Default: true. */ backoff?: boolean; /** * Add randomised jitter to retry delays to avoid thundering-herd problems. * When true, each delay is multiplied by a random factor in [0.5, 1.0]. * Default: false. */ jitter?: boolean; /** Status codes that trigger a retry. Default: [429, 500, 502, 503, 504]. */ retryOn?: number[]; /** * Called before each retry attempt. * @param attempt 1-based attempt number (1 = first retry). * @param error The error that triggered the retry. * @param delay The computed delay in ms before the retry fires. */ onRetry?: (attempt: number, error: HttpError, delay: number) => void; } interface OutgoingRequest { /** HTTP method. Supports all standard methods plus arbitrary strings for escape-hatch use. */ method: HttpMethod | string; url: string; headers: Record; body?: BodyInit; } type BeforeInterceptor = (req: OutgoingRequest, /** * The original per-request options passed to `http.get/post/…` or `http.request()`. * Read-only snapshot — mutating it has no effect on the in-flight request. * * Use this to inspect `params`, `timeout`, `responseType`, or any other option * that was supplied by the caller but is not yet reflected in `req.url` / `req.headers`. * * @example * http.before((req, options) => { * if (options.timeout && options.timeout < 1000) { * console.warn('Very short timeout:', options.timeout); * } * }); */ options: Readonly) => OutgoingRequest | void | Promise; interface AfterInterceptorContext { /** * Re-fire the original request from scratch — re-runs auth, before-interceptors, * and the full request pipeline with freshly-resolved credentials. * * Typical use case: catch a 401, refresh the token, call `replay()` to retry. * * **Infinite-loop guard:** inside a replayed request's after-interceptors, * `replay()` is a no-op that immediately returns the current result unchanged. * * @example * http.after(async (result, { replay }) => { * if (result.error?.isUnauthorized) { * await refreshToken(); * return replay(); * } * }); */ replay(): Promise>; } type AfterInterceptor = (result: HttpResult, context: AfterInterceptorContext) => HttpResult | void | Promise | void>; type HttpEvent = "request" | "response" | "error"; type HttpEventPayload = { request: OutgoingRequest; response?: HttpResult; }; type HttpEventHandler = (payload: HttpEventPayload) => void; /** * Every Http method returns this discriminated union. * * const { data, error, headers, request } = await http.get('/users'); * if (error) { ... } // data is null * // data is User[] here */ type HttpResult = { data: T; error: null; status: number; response: Response; /** Response headers as a plain object for easy access and serialisation. */ headers: Record; /** The outgoing request that produced this result. */ request: OutgoingRequest; } | { data: null; error: HttpError; status: number | null; response: Response | null; /** Response headers, or null when no response was received. */ headers: Record | null; /** The outgoing request that produced this result. */ request: OutgoingRequest; }; interface HttpConfig { /** Base URL prepended to every request path. */ baseURL?: string; /** * Authorization header value or a factory called before each request. * Return null/undefined to skip the header for that request. */ auth?: string | ((req: OutgoingRequest) => string | null | undefined); /** * Convert PUT requests to POST + append `putMethodKey=PUT` in the body. * Useful for Laravel-style file uploads. * @default false */ putToPost?: boolean; /** * Key appended to the body when `putToPost` is true. * @default "_method" */ putMethodKey?: string; /** Default timeout in ms. No timeout by default. */ timeout?: number; /** Default headers merged into every request. */ headers?: Record; /** * Default query parameters appended to every request. * Per-request `params` are merged on top (per-request wins on conflict). * * @example * const http = new Http({ baseURL, params: { api_key: 'xyz', version: 'v2' } }); * // Every request automatically includes ?api_key=xyz&version=v2 */ params?: HttpParams; /** * Controls whether cookies and HTTP authentication are sent with requests. * * - `"same-origin"` *(default)* — cookies sent only to same-origin URLs. * - `"include"` — cookies sent to all URLs, including cross-origin. * The server must respond with `Access-Control-Allow-Credentials: true`. * - `"omit"` — cookies never sent. * * **Browser note:** `"include"` is required for cross-origin cookie-based auth. * **Node.js note:** the native fetch cookie jar is not used; manage cookies * manually via `headers: { Cookie: '...' }` and read `Set-Cookie` from * `result.headers['set-cookie']`. */ credentials?: RequestCredentials; /** * CORS mode for all requests. * * - `"cors"` *(default)* — cross-origin requests are allowed; server must send CORS headers. * - `"no-cors"` — cross-origin request with limited response access (opaque response). * - `"same-origin"` — only same-origin requests; cross-origin throws. * - `"navigate"` — used by browser navigation; rarely needed in API clients. */ mode?: RequestMode; /** * Keep the underlying TCP connection alive after the page unloads. * Useful for fire-and-forget analytics / telemetry beacons sent on `unload`/`pagehide`. * Maximum body size is 64 KB when `keepalive` is true. * @default false */ keepalive?: boolean; /** * How to handle HTTP redirects. * - `"follow"` *(default)* — automatically follow redirects. * - `"error"` — throw a network error on any redirect. * - `"manual"` — return the redirect response opaquely (status 0) for manual handling. */ redirect?: RequestRedirect; /** * Enable/configure response caching for GET requests. * * Opt-in: with no `driver` configured the cache never runs at all. * * > ⚠️ **The built-in cache is a client-side feature.** Its default key is URL + params, * > which does not distinguish one caller from another — fine in a browser (one user per * > instance), unsafe on a server where a cached authenticated response could be served * > to a different user. Outside a browser an enabled cache therefore **requires** an * > identity-aware key: either `cache.generateKey` or a per-request `cacheKey`. Without * > one, the request throws rather than silently sharing data. * > * > On a server, prefer the framework's own cache (Next.js' data cache via * > `fetchInit: { next: { revalidate, tags } }`) or a store keyed per request. */ cache?: boolean | HttpCacheConfig; /** Retry failed requests. */ retry?: HttpRetryConfig; /** * Key used by Resource.publish(). * @default "published" */ publishKey?: string; /** * Custom body serializer. Replaces the default `JSON.stringify` for plain * object/array payloads. Has no effect on `FormData`, `Blob`, or `string` * bodies — those are always passed through unchanged. * * Use this to send MessagePack, CBOR, protobuf, or any other wire format * without wrapping every call site manually. * * @example * import { encode } from '@msgpack/msgpack'; * const http = new Http({ * baseURL, * serializer: (data) => ({ * body: encode(data), * contentType: 'application/msgpack', * }), * }); */ serializer?: (data: unknown) => { body: BodyInit; contentType: string; }; /** * Fetch-native cache directive (`RequestCache`), distinct from the * application-level `cache` option (which manages an external cache driver). * * - `"default"` *(browser default)* — honour the HTTP cache per standard rules. * - `"no-store"` — bypass the browser cache entirely; never read or write. * - `"reload"` — ignore any cached copy; update the cache after the request. * - `"no-cache"` — revalidate with the server before using a cached copy. * - `"force-cache"` — use a cached response even if stale. * - `"only-if-cached"` — fail if nothing is cached. * * Forwarded directly to every underlying `fetch()` call. * * @example * const http = new Http({ baseURL, fetchCache: 'no-store' }); // bypass browser cache */ fetchCache?: RequestCache; /** * Share a single underlying fetch between concurrent GET requests that resolve * to the same key. * * **Default: `true` in a browser, `false` everywhere else** (detected via * `typeof window === "undefined"`). * * Deduplication is a single-user optimisation. The key is derived from URL + params * and cannot include credentials, so on a server — SSR, a Node service, a worker, * anything multi-tenant — two concurrent `GET /me` calls carrying *different* bearer * tokens would otherwise share one fetch and both callers would receive the first * caller's response. The default is therefore off outside the browser. * * Setting this to `true` outside a browser is allowed but each deduplicated GET must * then carry an explicit `dedupeKey` **string** in its request options — you supply * the identity, because the library cannot know which header carries it in your app. * A GET without one throws instead of silently sharing. * * @example * const http = new Http({ baseURL, dedupe: false }); // disable wholesale */ dedupe?: boolean; /** * Custom deduplication key generator for concurrent GET requests. * * By default the dedup key is the full URL with all query params appended, * so two calls with different `params` use different in-flight slots. * Override to share a single underlying fetch across calls that differ * only in params, or implement any other custom keying strategy. * * > ⚠️ This receives **only** the URL and params — never headers, cookies or request * > options — so it cannot express "same URL, different user". It is a browser-side * > tuning knob, not a way to make deduplication safe on a server. For that, pass a * > per-request `dedupeKey` string instead. * * @param url The fully-resolved URL (baseURL + path, no query string). * @param params The merged params object (global defaults + per-request). * * @example * // Deduplicate on URL only — page=1 and page=2 share one in-flight fetch. * const http = new Http({ baseURL, dedupeKey: (url) => url }); */ dedupeKey?: (url: string, params?: HttpParams) => string; /** * Extra `RequestInit` fields forwarded verbatim to every underlying `fetch()` call. * * The library builds its init object from a fixed set of keys, so runtime- or * framework-specific ones it doesn't know about would otherwise be dropped. Use this * for Next.js' `next: { revalidate, tags }`, Deno/Bun/Cloudflare-specific keys, or * anything else the built-in set doesn't cover. Per-request `fetchInit` is merged on * top of this one (per-request wins on conflict). * * **Fields the library manages always win** — `method`, `headers`, `body` and `signal` * can never be overridden from here (that would bypass interceptors and auth, or break * cancellation and timeouts), and `credentials` / `mode` / `keepalive` / `redirect` / * `fetchCache` win whenever they are actually configured. Anything the library has no * opinion on passes straight through. * * @example * // Next.js App Router — tag every response so revalidateTag() can invalidate it. * const http = new Http({ baseURL, fetchInit: { next: { revalidate: 60, tags: ['news'] } } }); */ fetchInit?: RequestInit & Record; } /** * Controls how the response body is decoded. * Default: auto-detect from Content-Type (json → JSON, otherwise text). * * | Value | `data` type | Notes | * |-----------------|------------------|------------------------------------------------| * | `"json"` | `T` | Force JSON parse regardless of Content-Type. | * | `"text"` | `string` | Force text regardless of Content-Type. | * | `"blob"` | `Blob` | Browser file download / object URL. | * | `"arrayBuffer"` | `ArrayBuffer` | Binary buffer; wrap in `Buffer.from()` in Node.| * | `"stream"` | `ReadableStream` | **Raw byte stream — body is not read at all.** | * * ### `"stream"` — raw ReadableStream * * Returns `response.body` (the Web Streams API `ReadableStream`) untouched. * Use this for large file downloads or when you need to pipe the response somewhere. * * **Node.js:** convert to a Node.js `Readable` for `.pipe()` support: * ```ts * import { Readable } from 'stream'; * const { data } = await http.get('/file.zip', { responseType: 'stream' }); * Readable.fromWeb(data as ReadableStream).pipe(writeStream); * ``` * * **Browser:** use a `ReadableStream` directly or wrap in a `Response`: * ```ts * const { data } = await http.get('/video.mp4', { responseType: 'stream' }); * const blob = await new Response(data).blob(); * ``` * * > `onDownloadProgress` and `cache` are silently ignored in stream mode — * > the body must reach the caller unopened. */ type ResponseType = "json" | "text" | "blob" | "arrayBuffer" | "stream"; interface DownloadProgressEvent { /** Bytes received so far. */ loaded: number; /** Total bytes expected. null when the server omits Content-Length. */ total: number | null; /** 0–100, or null when total is unknown. */ percent: number | null; } interface UploadProgressEvent { /** Bytes sent so far. */ loaded: number; /** * Total bytes in the request body. * `null` for FormData bodies — the browser doesn't expose the serialized size * until transmission begins. Accurate for string / ArrayBuffer bodies. */ total: number | null; /** 0–100, or null when total is unknown. */ percent: number | null; } /** * How each line / event of a streamed response is parsed. * - "sse" — full Server-Sent Events parsing: id/event/data/retry fields, * multi-line data concatenation, [DONE] skipping, JSON parsing. * - "ndjson" — skips empty lines, JSON-parses each line. */ type StreamFormat = "sse" | "ndjson"; /** * A parsed Server-Sent Event with all fields. * Yielded by `Http.streamEvents()` when you need the full SSE envelope. */ interface SseEvent { /** The `id:` field. Tracked as `Last-Event-ID` on reconnect. */ id?: string; /** The `event:` field (named event type). Undefined for un-named events. */ event?: string; /** Parsed data payload. */ data: T; } interface StreamRequestOptions extends Omit { /** HTTP method. Default: "GET". Use "POST" for chat-style APIs. */ method?: HttpMethod; /** Request body for streaming POST/PUT. */ data?: HttpData; /** Line parsing strategy. Default: "sse". */ format?: StreamFormat; /** * Custom line parser. Return undefined to skip a line. * When provided, overrides the built-in SSE / NDJSON parsers. */ parseLine?: (line: string) => unknown; /** * Automatically reconnect when the SSE stream disconnects. * Default: `false`. Set to `true` to enable proper SSE auto-reconnection. * Reconnections send a `Last-Event-ID` header so the server can resume. * Does NOT reconnect on non-2xx HTTP errors (wrong path, auth failure, etc.). */ reconnect?: boolean; /** * Maximum number of reconnection attempts before giving up. * Default: unlimited. */ maxReconnectAttempts?: number; /** * Base delay in ms between reconnection attempts. * Overridden by the server's `retry:` directive if present. * Default: 3000. */ reconnectDelay?: number; } interface RequestOptions { /** Query string parameters. */ params?: HttpParams; /** Per-request headers merged on top of global headers. */ headers?: Record; /** External AbortSignal (React Query, useEffect cleanup, etc.). */ signal?: AbortSignal; /** Override global cache setting for this request. */ cache?: boolean | Omit & { driver?: CacheDriver; }; /** * Explicit cache key override. * * Outside a browser this (or `cache.generateKey`) is **required** whenever the cache is * enabled — the default URL + params key cannot tell two callers apart. Build it from * whatever identifies the caller in your app: * * @example * await http.get('/me', { cacheKey: `me:${session.userId}` }); */ cacheKey?: string; /** * Override the global `dedupe` setting for this single request. * See `HttpConfig.dedupe` for full documentation. */ dedupe?: boolean; /** * Explicit deduplication key for this GET request. * * Concurrent GETs that resolve to the same key share one underlying fetch, so this key * must capture **everything that makes two callers different** — most importantly their * identity. The library deliberately does not guess: it cannot know whether identity * lives in `Authorization`, a session cookie, an `X-Tenant-Id` header or somewhere else * in your app, and a wrong guess would look safe while leaking. * * Required for every deduplicated GET when `dedupe` is enabled outside a browser. * Takes precedence over `HttpConfig.dedupeKey`. * * @example * await http.get('/me', { dedupeKey: `me:${session.userId}` }); */ dedupeKey?: string; /** Override global retry setting for this request. */ retry?: boolean | Partial; /** * When true, throw HttpError instead of returning { data: null, error }. * Useful for try/catch boundaries. * @default false */ throw?: boolean; /** Per-request timeout override in ms. */ timeout?: number; /** * Override the global `credentials` setting for this single request. * See `HttpConfig.credentials` for full documentation. */ credentials?: RequestCredentials; /** * Override the global `mode` setting for this single request. * See `HttpConfig.mode` for full documentation. */ mode?: RequestMode; /** * Override the global `keepalive` setting for this single request. * See `HttpConfig.keepalive` for full documentation. */ keepalive?: boolean; /** * Override the global `redirect` setting for this single request. * See `HttpConfig.redirect` for full documentation. */ redirect?: RequestRedirect; /** Request body — used for DELETE-with-body (bulkDelete) and similar. */ data?: unknown; /** * How to decode the response body. * Default: auto-detect from Content-Type (json or text). */ responseType?: ResponseType; /** * Called each time a download chunk arrives. * When set, the body is read as a stream instead of buffered at once. */ onDownloadProgress?: (event: DownloadProgressEvent) => void; /** * Called each time a chunk of the request body is sent. * * **Accurate progress** is available for `string` and `ArrayBuffer` bodies * (the body is wrapped in a `ReadableStream` and chunked at 64 KiB intervals). * * **FormData bodies** do not support accurate progress because the browser * serializes them internally without exposing the size. No events will fire * for FormData — use a library like `axios` if you need FormData upload progress. * * Requires `duplex: "half"` fetch support (Chrome 105+, Node.js 18+, * Safari 17.4+). Environments without it fall back to no progress events. */ onUploadProgress?: (event: UploadProgressEvent) => void; /** * Per-request override of the fetch-native cache directive. * See `HttpConfig.fetchCache` for full documentation. */ fetchCache?: RequestCache; /** * Extra `RequestInit` fields forwarded verbatim to this request's `fetch()` call, * merged on top of `HttpConfig.fetchInit`. * See `HttpConfig.fetchInit` for full documentation and the override rules. * * @example * // Next.js App Router — tag this response so revalidateTag('news') invalidates it. * await http.get('/news', { fetchInit: { next: { tags: ['news'] } } }); */ fetchInit?: RequestInit & Record; } //#endregion export { AfterInterceptor, AfterInterceptorContext, BeforeInterceptor, CacheDriver, DownloadProgressEvent, HttpCacheConfig, HttpConfig, HttpData, HttpEvent, HttpEventHandler, HttpEventPayload, HttpMethod, HttpParams, HttpResult, HttpRetryConfig, OutgoingRequest, RequestOptions, ResponseType, SseEvent, StreamFormat, StreamRequestOptions, UploadProgressEvent }; //# sourceMappingURL=Http.types.d.mts.map