@openstage/monadyssey-fetch 3.0.0-beta.7 → 3.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/dist/monadyssey-fetch.d.ts +193 -96
- package/dist/monadyssey-fetch.mjs +151 -70
- package/dist/monadyssey-fetch.mjs.map +1 -1
- package/package.json +12 -8
- package/readme.md +27 -8
- package/dist/monadyssey-fetch.cjs +0 -8
- package/dist/monadyssey-fetch.cjs.map +0 -1
- package/dist/monadyssey-fetch.umd.js +0 -8
- package/dist/monadyssey-fetch.umd.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.mjs","names":[],"sources":["../src/http-error.ts","../src/http-request.ts","../src/http-response.ts","../src/http-transport.ts","../src/interceptors.ts","../src/sse.ts","../src/sse-client.ts","../src/http-client.ts"],"sourcesContent":["import { Option, Some } from \"@openstage/monadyssey-core\";\n\n/**\n * How a request failed.\n * - `\"request\"`: the request could not be built — typically the body could not be serialized.\n * Nothing was sent.\n * - `\"response\"`: the server answered with a non-2xx status, or a 2xx whose body could not be parsed.\n * - `\"network\"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.\n * - `\"timeout\"`: the client's `timeout` option fired before the response, or its body, completed.\n * The request may have reached the server.\n * - `\"aborted\"`: the request was aborted by something other than IO cancellation, such as an\n * interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as\n * `Cancelled`, not as an `HttpError`).\n */\nexport type HttpErrorKind = \"request\" | \"response\" | \"network\" | \"timeout\" | \"aborted\";\n\n/** @internal */\nexport const HttpErrorMessage = {\n describe: (cause: unknown): string =>\n cause instanceof Error\n ? cause.message\n : typeof cause === \"string\"\n ? cause\n : typeof cause === \"object\" && cause !== null && \"message\" in cause\n ? String((cause as { message: unknown }).message)\n : \"An unknown error occurred.\",\n\n /** Response errors keep the pre-3.0 wording; the other kinds carry no status. */\n format: (kind: HttpErrorKind, url: string, status: number, rawMessage: string, timeoutMs?: number): string => {\n switch (kind) {\n case \"response\":\n return `Request to '${url}' failed with status ${status} and message: ${rawMessage}.`;\n case \"request\":\n return `Request to '${url}' could not be sent: ${rawMessage}.`;\n case \"timeout\":\n return `Request to '${url}' timed out after ${timeoutMs} ms.`;\n case \"aborted\":\n return `Request to '${url}' was aborted before a response arrived: ${rawMessage}.`;\n default:\n return `Request to '${url}' failed before a response arrived: ${rawMessage}.`;\n }\n },\n};\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP\n * status code, raw error message, response body, request URL, response headers, and the\n * underlying cause.\n *\n * `kind` tells the two facts apart that `status` alone cannot: \"the server said 500\" is\n * `kind: \"response\", status: 500`, while \"nothing answered\" is `kind: \"network\", status: 0`.\n * For every kind other than `\"response\"`, `status` is `0` and `body` is `null`.\n *\n * @example\n * client.post(\"/payments\", payment).mapErr((error) =>\n * error.kind === \"response\"\n * ? { kind: \"rejected\", status: error.status }\n * : { kind: \"outcomeUnknown\" } // the request may or may not have reached the server\n * );\n */\nexport class HttpError extends Error {\n /** The transport outcome. See {@link HttpErrorKind}. */\n public readonly kind: HttpErrorKind;\n /** The HTTP status code, or `0` when no response arrived (`kind !== \"response\"`). */\n public readonly status: number;\n public readonly rawMessage: string;\n /** The parsed response body, or `null` when none was parsed. */\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n /** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */\n public readonly cause?: unknown;\n /** The timeout that fired, in milliseconds. Set only when `kind === \"timeout\"`. */\n public readonly timeoutMs?: number;\n\n constructor(\n status: number,\n rawMessage: string,\n body: unknown,\n url: string,\n headers?: Record<string, string>,\n kind: HttpErrorKind = \"response\",\n cause?: unknown,\n timeoutMs?: number\n ) {\n super(HttpErrorMessage.format(kind, url, status, rawMessage, timeoutMs));\n this.name = \"HttpError\";\n this.kind = kind;\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\n this.cause = cause;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * An error for a request that received no response: DNS failure, offline, CORS block, or a\n * rejected interceptor. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).\n */\n static network(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"network\", cause);\n }\n\n /**\n * An error for a request whose `timeout` option fired before the response, or its body, completed.\n * `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.\n *\n * @param {string} url The request URL.\n * @param {number} after The timeout in milliseconds.\n */\n static timeout(url: string, after: number): HttpError {\n return new HttpError(0, `Request timed out after ${after} ms`, null, url, undefined, \"timeout\", undefined, after);\n }\n\n /**\n * An error for a request that could not be built, so nothing was sent — typically a body that\n * could not be serialized. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).\n */\n static request(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"request\", cause);\n }\n\n /**\n * An error for a request aborted by something other than IO cancellation, such as an\n * interceptor aborting the signal. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} [cause] The abort reason, if any.\n */\n static aborted(url: string, cause?: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"aborted\", cause);\n }\n\n /**\n * Narrows the `unknown` error body to a concrete type using a runtime type guard.\n * Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,\n * not asserted. This keeps the unsafe cast out of consumer code: the body stays `unknown`\n * by default (forcing a conscious narrow), and this is the safe way to perform it.\n *\n * @template T The expected body shape.\n * @param {(x: unknown) => x is T} guard A type guard validating the body's shape.\n * @returns {Option<T>} `Some(body)` if the guard matches, `None` otherwise.\n *\n * @example\n * interface ApiError { code: string; message: string }\n * const isApiError = (x: unknown): x is ApiError =>\n * typeof x === \"object\" && x !== null && \"code\" in x && \"message\" in x;\n *\n * httpError.bodyAs(isApiError).fold(\n * () => \"unrecognized error shape\",\n * (e) => `${e.code}: ${e.message}`\n * );\n */\n bodyAs<T>(guard: (x: unknown) => x is T): Option<T> {\n return guard(this.body) ? (Some.of(this.body as NonNullable<T>) as Option<T>) : Option.empty<T>();\n }\n}\n","import { Credentials, Method, Options } from \"./options\";\n\n/** @internal */\nexport interface RequestDefaults {\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n}\n\n/** @internal */\nexport interface PreparedRequest {\n readonly method: Method;\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n readonly body: BodyInit | undefined;\n}\n\n/** Bodies `fetch` sends as-is: never JSON-encoded, never given an automatic content type. */\nconst isNativeBody = (body: unknown): boolean =>\n typeof body !== \"object\" ||\n body === null ||\n body instanceof FormData ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof URLSearchParams ||\n (typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream) ||\n ArrayBuffer.isView(body);\n\nconst normalizeHeaders = (headers: Record<string, string | undefined>): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([key, value]) => [key.toLowerCase(), value])\n );\n\n/** `FormData` must carry no content type so the browser can set the multipart boundary. */\nconst withContentType = (headers: Record<string, string>, body: unknown): Record<string, string> => {\n if (body instanceof FormData) {\n const { \"content-type\": _dropped, ...rest } = headers;\n return rest;\n }\n const autoJson = body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in headers);\n return autoJson ? { ...headers, \"content-type\": \"application/json\" } : headers;\n};\n\nconst serializeBody = (method: Method, body: unknown, headers: Record<string, string>): BodyInit | undefined =>\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? (body as BodyInit)\n : headers[\"content-type\"] === \"application/json\"\n ? JSON.stringify(body)\n : (body as BodyInit);\n\n/** @internal */\nexport const HttpRequest = {\n isNativeBody,\n normalizeHeaders,\n\n /** Per-request headers win over defaults; names are lowercased; the body follows the content type. */\n prepare: (method: Method, options: Options<unknown>, defaults: RequestDefaults): PreparedRequest => {\n const headers = withContentType(\n normalizeHeaders({ ...defaults.headers, ...(options.headers ?? {}) }),\n options.body\n );\n return {\n method,\n headers,\n credentials: options.credentials ?? defaults.credentials,\n body: serializeBody(method, options.body, headers),\n };\n },\n};\n","import { IO, Option } from \"@openstage/monadyssey-core\";\nimport { HttpError, HttpErrorMessage } from \"./http-error\";\nimport { ResponseType } from \"./options\";\n\ntype BodyReader = (response: Response) => Promise<unknown>;\n\nconst bodyReaders = {\n json: (response) => response.json(),\n text: (response) => response.text(),\n blob: (response) => response.blob(),\n arrayBuffer: (response) => response.arrayBuffer(),\n formData: (response) => response.formData(),\n} satisfies Record<ResponseType, BodyReader>;\n\n/** An own-property lookup, so an untyped `responseType` such as `\"toString\"` is rejected. */\nconst readerFor = (responseType: ResponseType): Option<BodyReader> =>\n Object.prototype.hasOwnProperty.call(bodyReaders, responseType)\n ? Option.of(bodyReaders[responseType])\n : Option.empty<BodyReader>();\n\nconst headers = (response: Response): Record<string, string> => {\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** The server answered but the body could not be used; the real status is kept, the body is `null`. */\nconst error = (cause: unknown, url: string, response: Response): HttpError =>\n new HttpError(response.status, HttpErrorMessage.describe(cause), null, url, headers(response), \"response\", cause);\n\n/** The server answered with a non-2xx status; the parsed body travels with the error. */\nconst failure = (response: Response, body: unknown, url: string): HttpError =>\n new HttpError(response.status, response.statusText, body, url, headers(response));\n\n/** @internal */\nexport const HttpResponse = {\n headers,\n error,\n failure,\n\n /** `204` and `205` carry no body and yield `null`. */\n read: (response: Response, responseType: ResponseType, url: string): IO<HttpError, unknown> =>\n IO.from(readerFor(responseType), () => error(`Unsupported response type: ${responseType}`, url, response)).flatMap(\n (read) =>\n IO.lift(\n () => (response.status === 204 || response.status === 205 ? null : read(response)),\n (cause) => error(cause, url, response)\n )\n ),\n\n settle: (response: Response, parsed: unknown, url: string): IO<HttpError, unknown> =>\n response.ok ? IO.of(parsed) : IO.fail(failure(response, parsed, url)),\n};\n","import { HttpError } from \"./http-error\";\n\nconst isAbort = (e: unknown): boolean =>\n typeof e === \"object\" && e !== null && \"name\" in e && (e as { name: unknown }).name === \"AbortError\";\n\n/** @internal */\nexport const HttpTransport = {\n /** An interceptor may reject with a ready-made `HttpError`; it is passed through untouched. */\n error: (e: unknown, url: string): HttpError =>\n e instanceof HttpError ? e : isAbort(e) ? HttpError.aborted(url, e) : HttpError.network(url, e),\n\n /** A timeout means the caller stopped waiting and an abort means someone wanted it stopped: neither is retried. */\n isRetryable: (error: HttpError): boolean =>\n error.kind === \"network\" || (error.kind === \"response\" && error.status >= 500),\n};\n","import { HttpInterceptor } from \"./options\";\n\n/** @internal */\nexport type Send = (request: RequestInit) => Promise<Response>;\n\n/** @internal */\nexport const Interceptors = {\n /** A right fold, so the first registered interceptor is outermost. */\n run: (interceptors: readonly HttpInterceptor[], request: RequestInit, send: Send): Promise<Response> =>\n interceptors.reduceRight<Send>((next, interceptor) => (r) => interceptor.intercept(r, next), send)(request),\n};\n","import { List, Stream } from \"@openstage/monadyssey-core\";\nimport type { Pipe } from \"@openstage/monadyssey-core\";\n\n/**\n * A single server-sent event parsed from a text/event-stream response.\n *\n * @property id - The event id, if the server sent an `id:` field. Used to resume a stream.\n * @property event - The event type. Defaults to `\"message\"` when the server sends no `event:` field.\n * @property data - The event payload. Multiple `data:` lines are joined with a newline.\n * @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.\n */\nexport interface ServerSentEvent {\n readonly id?: string;\n readonly event: string;\n readonly data: string;\n readonly retry?: number;\n}\n\n/** `null` when the block dispatches no event (comment-only, or no `data:`), per the event-stream rules. */\nconst parseBlock = (block: string): ServerSentEvent | null => {\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n const data: string[] = [];\n\n for (const line of block.split(\"\\n\")) {\n if (line === \"\" || line.startsWith(\":\")) continue; // blank line or comment\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n\n switch (field) {\n case \"event\":\n event = value;\n break;\n case \"data\":\n data.push(value);\n break;\n case \"id\":\n id = value;\n break;\n case \"retry\":\n if (/^\\d+$/.test(value)) retry = Number.parseInt(value, 10);\n break;\n default:\n break;\n }\n }\n\n if (data.length === 0) return null;\n return {\n event: event ?? \"message\",\n data: data.join(\"\\n\"),\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n };\n};\n\n/**\n * Reassembles a byte stream into complete events; partial input and split multi-byte characters\n * are buffered. Call `reset` when a new connection begins.\n *\n * @internal\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n push(chunk: Uint8Array): ServerSentEvent[] {\n let text = this.buffered + this.decoder.decode(chunk, { stream: true });\n let pendingCR = false;\n if (text.endsWith(\"\\r\")) {\n text = text.slice(0, -1); // hold a trailing CR: it may be the first half of a split CRLF\n pendingCR = true;\n }\n text = text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const blocks = text.split(\"\\n\\n\");\n let rest = blocks.pop() ?? \"\";\n if (pendingCR) rest += \"\\r\";\n this.buffered = rest;\n return blocks.map(parseBlock).filter((event): event is ServerSentEvent => event !== null);\n }\n\n reset(): void {\n this.decoder = new TextDecoder(\"utf-8\");\n this.buffered = \"\";\n }\n}\n\n/**\n * A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering\n * partial input across chunks. Single-use: each application maintains its own decoding state.\n *\n * @typeParam E - The error type carried by the surrounding stream.\n * @returns A pipe from a byte stream to a stream of parsed events.\n */\nexport const decodeEvents = <E>(): Pipe<E, Uint8Array, ServerSentEvent> => {\n const frames = new FrameBuffer();\n return (bytes: Stream<E, Uint8Array>) =>\n bytes.mapAccumulate(null, (chunk) => [null, List.from(frames.push(chunk))] as const);\n};\n","import { IO, Option, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { Credentials, HttpInterceptor } from \"./options\";\nimport { FrameBuffer, ServerSentEvent } from \"./sse\";\n\n/** A server-sent `retry:` below this is raised to it, so a misbehaving server cannot force a tight loop. */\nconst MIN_SERVER_RETRY_MS = 100;\n\ntype Reader = ReadableStreamDefaultReader<Uint8Array>;\n\n/** A cancellable sleep. The abort listener is removed when the timer fires, so waits do not accumulate listeners. */\nconst delay = (ms: number): IO<never, void> =>\n IO.cancellable<never, void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/**\n * Carried across reconnections within one run. Owns the one `AbortController` every connection\n * of the run is opened with, linked once to the run's signal; a connection registers a single\n * listener on it and removes it when closed, so listeners stay bounded however long the run lasts.\n */\nclass SseSession {\n readonly controller = new AbortController();\n readonly frames = new FrameBuffer();\n readonly pending: ServerSentEvent[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n private connection: { readonly reader: Reader; readonly unlink: () => void } | null = null;\n private readonly unlinkParent: () => void;\n\n constructor(retryMs: number, parent: AbortSignal) {\n this.retryMs = retryMs;\n const onAbort = () => this.controller.abort(parent.reason);\n parent.addEventListener(\"abort\", onAbort, { once: true });\n this.unlinkParent = () => parent.removeEventListener(\"abort\", onAbort);\n }\n\n get reader(): Reader | null {\n return this.connection?.reader ?? null;\n }\n\n attach(reader: Reader): void {\n const onAbort = () => reader.cancel().catch(() => {});\n this.controller.signal.addEventListener(\"abort\", onAbort, { once: true });\n this.connection = { reader, unlink: () => this.controller.signal.removeEventListener(\"abort\", onAbort) };\n this.frames.reset();\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.unlink();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n this.unlinkParent();\n this.controller.abort();\n }\n}\n\n/** @internal */\nexport interface SseConfig {\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly defaultHeaders: Record<string, string>;\n readonly credentials: Credentials;\n readonly interceptors: readonly HttpInterceptor[];\n readonly reconnect: boolean;\n readonly retry: number;\n}\n\nconst stream = (config: SseConfig): Stream<HttpError, ServerSentEvent> => {\n const { url, headers, defaultHeaders, credentials, interceptors, reconnect, retry } = config;\n\n const open = (session: SseSession): IO<HttpError, Reader> => {\n const prepared = HttpRequest.prepare(\n \"GET\",\n {\n headers: {\n ...headers,\n accept: \"text/event-stream\",\n ...(session.lastEventId !== undefined ? { \"last-event-id\": session.lastEventId } : {}),\n },\n credentials,\n },\n { headers: defaultHeaders, credentials }\n );\n return IO.lift<HttpError, Response>(\n () =>\n Interceptors.run(interceptors, { ...prepared, signal: session.controller.signal }, (req) => fetch(url, req)),\n (e: unknown) => HttpTransport.error(e, url)\n )\n .flatMap((response) =>\n response.ok\n ? IO.of(response)\n : HttpResponse.read(response, \"text\", url)\n .flatMapErr(() => IO.of<unknown>(null))\n .flatMap((body) => IO.fail<HttpError, Response>(HttpResponse.failure(response, body, url)))\n )\n .flatMap((response) =>\n response.body === null\n ? IO.fail<HttpError, Reader>(HttpResponse.error(\"Response has no body\", url, response))\n : IO.of(response.body.getReader())\n );\n };\n\n const readChunk = (reader: Reader): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.lift(\n () => reader.read(),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\n const done = IO.of<EventStep>(Option.empty());\n\n const retryLater = (session: SseSession): IO<HttpError, EventStep> =>\n delay(session.retryMs).flatMap(() => step(session));\n\n const step = (session: SseSession): IO<HttpError, EventStep> => {\n if (session.pending.length > 0) {\n const event = session.pending.shift() as ServerSentEvent;\n if (event.id !== undefined) session.lastEventId = event.id;\n if (event.retry !== undefined) session.retryMs = Math.max(event.retry, MIN_SERVER_RETRY_MS);\n return IO.of(Option.of<[ServerSentEvent, SseSession]>([event, session]));\n }\n\n const reader = session.reader;\n if (reader === null) {\n return open(session).foldM<HttpError, EventStep>(\n (error) => (reconnect && HttpTransport.isRetryable(error) ? retryLater(session) : IO.fail(error)),\n (opened) => {\n session.attach(opened);\n return step(session);\n }\n );\n }\n\n return readChunk(reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n return reconnect && HttpTransport.isRetryable(error) ? retryLater(session) : IO.fail(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n return reconnect ? retryLater(session) : done;\n }\n session.pending.push(...session.frames.push(result.value));\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.cancellable((signal: AbortSignal) => new SseSession(retry, signal)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.lift(() => session.close())\n );\n};\n\n/** @internal */\nexport const SseClient = { stream };\n","import { IO, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest, PreparedRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { SseClient } from \"./sse-client\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, SseOptions } from \"./options\";\nimport { ServerSentEvent } from \"./sse\";\n\n/**\n * A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of Promises.\n *\n * Unlike v1, `HttpClient` is instantiable — each instance carries its own configuration (base URL,\n * interceptors, default headers, timeout, credentials). This allows different parts of an application\n * to use independently configured clients.\n *\n * All HTTP methods return `IO<HttpError, A | null>`, enabling lazy execution, functional composition,\n * and explicit error handling. Cancellation is supported: when an IO is cancelled (via fiber or\n * timeout), the underlying `fetch` call is aborted through `AbortSignal`.\n *\n * **On the `| null` in the return type:** when the server responds with `204 No Content` or\n * `205 Reset Content`, there is no body to parse and the IO succeeds with `null`. This is a\n * protocol-level fact (the HTTP spec says these statuses have no body), distinct from a\n * domain-level optionality. For that reason the methods return `IO<HttpError, A | null>` rather\n * than `IO<HttpError, Option<A>>`. `Option` is the right tool when a value might be absent at\n * the domain level; `| null` is honest when the absence is structural to the protocol. For the\n * 95% case where you control the endpoint and never receive 204, the `| null` is a brief\n * `?? defaultValue` away.\n *\n * **Default credentials:** `same-origin`, matching the platform `fetch` default. Set\n * `credentials: \"include\"` explicitly per-client or per-request if you need cookies to flow\n * cross-origin.\n *\n * @example\n * const api = new HttpClient({\n * baseUrl: \"https://api.example.com\",\n * interceptors: [authInterceptor],\n * defaultHeaders: { \"Accept\": \"application/json\" },\n * timeout: 5000,\n * });\n *\n * const users = api.get<User[]>(\"/users\");\n */\nexport class HttpClient {\n private readonly interceptors: readonly HttpInterceptor[];\n private readonly baseUrl: string | undefined;\n private readonly defaultHeaders: Record<string, string>;\n private readonly defaultTimeout: number | undefined;\n private readonly defaultCredentials: Credentials;\n\n /**\n * Creates a new HttpClient with the given configuration.\n *\n * @param {HttpClientConfig} config - Configuration for the client.\n */\n constructor(config: HttpClientConfig = {}) {\n this.interceptors = Object.freeze([...(config.interceptors ?? [])]);\n this.baseUrl = config.baseUrl;\n this.defaultHeaders = config.defaultHeaders ?? {};\n this.defaultTimeout = config.timeout;\n this.defaultCredentials = config.credentials ?? \"same-origin\";\n }\n\n /**\n * Performs a GET request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Omit<Options<A>, \"body\">} [options] - Request options (excluding body).\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n get(uri: string, options: Omit<Options, \"body\"> & { observe: \"response\" }): IO<HttpError, Response>;\n get<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, A | null>;\n get<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"GET\", options);\n }\n\n /**\n * Performs a POST request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n post(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n post<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n post<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"POST\", { ...options, body });\n }\n\n /**\n * Performs a PUT request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n put(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n put<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n put<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"PUT\", { ...options, body });\n }\n\n /**\n * Performs a PATCH request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n patch(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n patch<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n patch<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"PATCH\", { ...options, body });\n }\n\n /**\n * Performs a DELETE request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Omit<Options<A>, \"body\">} [options] - Request options (excluding body).\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n delete(uri: string, options: Omit<Options, \"body\"> & { observe: \"response\" }): IO<HttpError, Response>;\n delete<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, A | null>;\n delete<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"DELETE\", options);\n }\n\n /**\n * Performs a custom HTTP request with the specified method.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Method} method - The HTTP method.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n fetch(uri: string, method: Method, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n fetch<A = unknown>(uri: string, method: Method, options?: Options<A>): IO<HttpError, A | null>;\n fetch<A = unknown>(uri: string, method: Method, options: Options<A> = {}): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, method, options);\n }\n\n /**\n * Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.\n *\n * The request goes through the same pipeline as every other method, so headers (including\n * `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is\n * used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts\n * the connection and releases resources.\n *\n * By default the stream reconnects automatically when the connection ends or fails with a\n * retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers\n * and credentials on each attempt; terminal `4xx` responses (including auth failures) end the\n * stream. See {@link SseOptions} to configure or disable this.\n *\n * @param {string} uri - The URL or path to connect to.\n * @param {SseOptions} [options] - Headers, credentials, and reconnection settings.\n * @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.\n *\n * @example\n * const events = client.sse(\"/events\", { headers: { Authorization: `Bearer ${token}` } });\n * await events.runForEach((event) => IO.lift(() => handle(event))).unsafeRun();\n */\n sse(uri: string, options: SseOptions = {}): Stream<HttpError, ServerSentEvent> {\n return SseClient.stream({\n url: this.resolveUrl(uri),\n headers: options.headers ?? {},\n defaultHeaders: this.defaultHeaders,\n credentials: options.credentials ?? this.defaultCredentials,\n interceptors: this.interceptors,\n reconnect: options.reconnect ?? true,\n retry: options.retry ?? 3000,\n });\n }\n\n private resolveUrl(uri: string): string {\n if (!this.baseUrl) return uri;\n try {\n new URL(uri);\n return uri;\n } catch {\n const base = this.baseUrl.endsWith(\"/\") ? this.baseUrl.slice(0, -1) : this.baseUrl;\n const path = uri.startsWith(\"/\") ? uri : `/${uri}`;\n return `${base}${path}`;\n }\n }\n\n private request<A = unknown>(\n uri: string,\n method: Method,\n options: Options<A> = {}\n ): IO<HttpError, Response | A | null> {\n const url = this.resolveUrl(uri);\n const timeout = options.timeout ?? this.defaultTimeout;\n const { responseType = \"json\", observe = \"body\", transform = (data: unknown) => data as A } = options;\n const defaults = { headers: this.defaultHeaders, credentials: this.defaultCredentials };\n\n const prepare: IO<HttpError, PreparedRequest> = IO.lift(\n () => HttpRequest.prepare(method, options, defaults),\n (e: unknown) => HttpError.request(url, e)\n );\n\n const send = (prepared: PreparedRequest): IO<HttpError, Response> =>\n IO.cancellable(\n (signal: AbortSignal) => Interceptors.run(this.interceptors, { ...prepared, signal }, (req) => fetch(url, req)),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n const decode = (response: Response): IO<HttpError, A | null> =>\n HttpResponse.read(response, responseType, url)\n .flatMap((parsed) => HttpResponse.settle(response, parsed, url))\n .flatMap((parsed) =>\n IO.lift(\n () => transform(parsed),\n (e: unknown) => HttpResponse.error(e, url, response)\n )\n );\n\n const outcome = prepare\n .flatMap(send)\n .flatMap((response): IO<HttpError, Response | A | null> =>\n response.ok && observe === \"response\" ? IO.of(response) : decode(response)\n );\n\n return timeout == null ? outcome : outcome.timeout(timeout, () => HttpError.timeout(url, timeout));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"monadyssey-fetch.mjs","names":[],"sources":["../src/http-error.ts","../src/http-request.ts","../src/http-response.ts","../src/http-transport.ts","../src/interceptors.ts","../src/sse.ts","../src/sse-client.ts","../src/http-client.ts"],"sourcesContent":["import { Option } from \"@openstage/monadyssey-core\";\n\n/**\n * How a request failed.\n * - `\"request\"`: the request could not be built — typically the body could not be serialized.\n * Nothing was sent.\n * - `\"response\"`: the server answered with a non-2xx status. `body` carries what it sent.\n * - `\"decode\"`: the server answered 2xx, but the body could not be used — it failed to parse, a\n * `transform` threw, or there was no body where one was expected (a `204`, say). `status` is the\n * real status and `cause` the underlying error.\n * - `\"network\"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.\n * - `\"timeout\"`: the client's `timeout` option fired before the response, or its body, completed.\n * The request may have reached the server.\n * - `\"aborted\"`: the request was aborted by something other than IO cancellation, such as an\n * interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as\n * `Cancelled`, not as an `HttpError`).\n *\n * New kinds may be added in a minor release, so give an exhaustive `switch` a `default` branch.\n */\nexport type HttpErrorKind = \"request\" | \"response\" | \"decode\" | \"network\" | \"timeout\" | \"aborted\";\n\n/** @internal */\nexport const HttpErrorMessage = {\n describe: (cause: unknown): string =>\n cause instanceof Error\n ? cause.message\n : typeof cause === \"string\"\n ? cause\n : typeof cause === \"object\" && cause !== null && \"message\" in cause\n ? String((cause as { message: unknown }).message)\n : \"An unknown error occurred.\",\n\n /** Response errors keep the pre-3.0 wording; the other kinds carry no status. */\n format: (kind: HttpErrorKind, url: string, status: number, rawMessage: string, timeoutMs?: number): string => {\n switch (kind) {\n case \"response\":\n return `Request to '${url}' failed with status ${status} and message: ${rawMessage}.`;\n case \"decode\":\n return `Response from '${url}' (status ${status}) could not be decoded: ${rawMessage}.`;\n case \"request\":\n return `Request to '${url}' could not be sent: ${rawMessage}.`;\n case \"timeout\":\n return `Request to '${url}' timed out after ${timeoutMs} ms.`;\n case \"aborted\":\n return `Request to '${url}' was aborted before a response arrived: ${rawMessage}.`;\n default:\n return `Request to '${url}' failed before a response arrived: ${rawMessage}.`;\n }\n },\n};\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP\n * status code, raw error message, response body, request URL, response headers, and the\n * underlying cause.\n *\n * `kind` tells the two facts apart that `status` alone cannot: \"the server said 500\" is\n * `kind: \"response\", status: 500`, while \"nothing answered\" is `kind: \"network\", status: 0`.\n * For every kind other than `\"response\"` and `\"decode\"`, `status` is `0`; `body` is `null` except\n * for `\"response\"`.\n *\n * @example\n * client.post(\"/payments\", payment).mapErr((error) =>\n * error.kind === \"response\"\n * ? { kind: \"rejected\", status: error.status }\n * : { kind: \"outcomeUnknown\" } // the request may or may not have reached the server\n * );\n */\nexport class HttpError extends Error {\n /** The transport outcome. See {@link HttpErrorKind}. */\n public readonly kind: HttpErrorKind;\n /** The HTTP status code, or `0` when no response arrived (`kind !== \"response\"`). */\n public readonly status: number;\n public readonly rawMessage: string;\n /** The parsed response body, or `null` when none was parsed. */\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n /** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */\n public readonly cause?: unknown;\n /** The timeout that fired, in milliseconds. Set only when `kind === \"timeout\"`. */\n public readonly timeoutMs?: number;\n\n constructor(\n status: number,\n rawMessage: string,\n body: unknown,\n url: string,\n headers?: Record<string, string>,\n kind: HttpErrorKind = \"response\",\n cause?: unknown,\n timeoutMs?: number\n ) {\n super(HttpErrorMessage.format(kind, url, status, rawMessage, timeoutMs));\n this.name = \"HttpError\";\n this.kind = kind;\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\n this.cause = cause;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * An error for a request that received no response: DNS failure, offline, CORS block, or a\n * rejected interceptor. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).\n */\n static network(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"network\", cause);\n }\n\n /**\n * An error for a request whose `timeout` option fired before the response, or its body, completed.\n * `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.\n *\n * @param {string} url The request URL.\n * @param {number} after The timeout in milliseconds.\n */\n static timeout(url: string, after: number): HttpError {\n return new HttpError(0, `Request timed out after ${after} ms`, null, url, undefined, \"timeout\", undefined, after);\n }\n\n /**\n * An error for a 2xx response whose body could not be used: it failed to parse, a `transform`\n * threw, or there was no body where one was expected. `status` is the real status, `body` is\n * `null`, and `cause` carries the underlying error.\n *\n * @param {string} url The request URL.\n * @param {Response} response The response that could not be decoded.\n * @param {unknown} cause The parse error, the value a `transform` threw, or a description.\n */\n static decode(url: string, response: Response, cause: unknown): HttpError {\n let headers: Record<string, string> = {};\n try {\n headers = Object.fromEntries(response.headers.entries());\n } catch {\n /* headers unavailable */\n }\n return new HttpError(response.status, HttpErrorMessage.describe(cause), null, url, headers, \"decode\", cause);\n }\n\n /**\n * An error for a request that could not be built, so nothing was sent — typically a body that\n * could not be serialized. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).\n */\n static request(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"request\", cause);\n }\n\n /**\n * An error for a request aborted by something other than IO cancellation, such as an\n * interceptor aborting the signal. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} [cause] The abort reason, if any.\n */\n static aborted(url: string, cause?: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"aborted\", cause);\n }\n\n /**\n * Narrows the `unknown` error body to a concrete type using a runtime type guard.\n * Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,\n * not asserted. This keeps the unsafe cast out of consumer code: the body stays `unknown`\n * by default (forcing a conscious narrow), and this is the safe way to perform it.\n *\n * @template T The expected body shape.\n * @param {(x: unknown) => x is T} guard A type guard validating the body's shape.\n * @returns {Option<T>} `Some(body)` if the guard matches, `None` otherwise.\n *\n * @example\n * interface ApiError { code: string; message: string }\n * const isApiError = (x: unknown): x is ApiError =>\n * typeof x === \"object\" && x !== null && \"code\" in x && \"message\" in x;\n *\n * httpError.bodyAs(isApiError).fold(\n * () => \"unrecognized error shape\",\n * (e) => `${e.code}: ${e.message}`\n * );\n */\n bodyAs<T>(guard: (x: unknown) => x is T): Option<NonNullable<T>> {\n return guard(this.body) ? Option.ofNullable(this.body) : Option.empty();\n }\n}\n","import { Credentials, Method, Options } from \"./options\";\n\n/** @internal */\nexport interface RequestDefaults {\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n}\n\n/** @internal */\nexport interface PreparedRequest {\n readonly method: Method;\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n readonly body: BodyInit | undefined;\n}\n\n/** Bodies `fetch` sends as-is: never JSON-encoded, never given an automatic content type. */\nconst isNativeBody = (body: unknown): boolean =>\n typeof body !== \"object\" ||\n body === null ||\n body instanceof FormData ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof URLSearchParams ||\n (typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream) ||\n ArrayBuffer.isView(body);\n\nconst normalizeHeaders = (headers: Record<string, string | undefined>): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([key, value]) => [key.toLowerCase(), value])\n );\n\n/** `FormData` must carry no content type so the browser can set the multipart boundary. */\nconst withContentType = (headers: Record<string, string>, body: unknown): Record<string, string> => {\n if (body instanceof FormData) {\n const { \"content-type\": _dropped, ...rest } = headers;\n return rest;\n }\n const autoJson = body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in headers);\n return autoJson ? { ...headers, \"content-type\": \"application/json\" } : headers;\n};\n\n/**\n * `application/json`, with or without parameters (`; charset=utf-8`), or any `+json` suffix type\n * (`application/merge-patch+json`, `application/vnd.api+json`). Matching the exact string alone\n * would send an object declared as one of the others as `\"[object Object]\"`.\n */\nconst isJsonContentType = (contentType: string | undefined): boolean => {\n if (contentType === undefined) return false;\n const media = contentType.split(\";\")[0].trim().toLowerCase();\n return media === \"application/json\" || media.endsWith(\"+json\");\n};\n\nconst serializeBody = (method: Method, body: unknown, headers: Record<string, string>): BodyInit | undefined =>\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? (body as BodyInit)\n : isJsonContentType(headers[\"content-type\"])\n ? JSON.stringify(body)\n : (body as BodyInit);\n\n/** @internal */\nexport const HttpRequest = {\n isNativeBody,\n normalizeHeaders,\n\n /** Per-request headers win over defaults; names are lowercased; the body follows the content type. */\n prepare: (method: Method, options: Options<unknown>, defaults: RequestDefaults): PreparedRequest => {\n const headers = withContentType(\n normalizeHeaders({ ...defaults.headers, ...(options.headers ?? {}) }),\n options.body\n );\n return {\n method,\n headers,\n credentials: options.credentials ?? defaults.credentials,\n body: serializeBody(method, options.body, headers),\n };\n },\n};\n","import { IO, Option } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { Method, ResponseType } from \"./options\";\n\ntype BodyReader = (response: Response) => Promise<unknown>;\n\nconst bodyReaders = {\n json: (response) => response.json(),\n text: (response) => response.text(),\n blob: (response) => response.blob(),\n arrayBuffer: (response) => response.arrayBuffer(),\n formData: (response) => response.formData(),\n} satisfies Record<ResponseType, BodyReader>;\n\n/** An own-property lookup, so an untyped `responseType` such as `\"toString\"` is rejected. */\nconst readerFor = (responseType: ResponseType): Option<BodyReader> =>\n Object.prototype.hasOwnProperty.call(bodyReaders, responseType)\n ? Option.of(bodyReaders[responseType])\n : Option.empty<BodyReader>();\n\nconst headers = (response: Response): Record<string, string> => {\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** The server answered 2xx but the body could not be used (`kind: \"decode\"`). */\nconst error = (cause: unknown, url: string, response: Response): HttpError => HttpError.decode(url, response, cause);\n\n/**\n * What {@link HttpResponse.read} yields when there is no body at all — distinct from a JSON body\n * that is literally `null`.\n *\n * @internal\n */\nexport const NO_BODY: unique symbol = Symbol(\"HttpResponse.noBody\");\n\n/** The server answered with a non-2xx status; the parsed body travels with the error. */\nconst failure = (response: Response, body: unknown, url: string): HttpError =>\n new HttpError(response.status, response.statusText, body, url, headers(response));\n\n/**\n * Reads a JSON body, yielding `NO_BODY` for an empty one. Responses that implement only `json()` —\n * hand-rolled mocks in consumers' tests — are read with it.\n */\nconst readJson = async (response: Response): Promise<unknown> => {\n if (typeof response.text !== \"function\") return response.json();\n const text = await response.text();\n return text.trim() === \"\" ? NO_BODY : JSON.parse(text);\n};\n\n/** @internal */\nexport const HttpResponse = {\n headers,\n error,\n failure,\n\n /**\n * Reads a successful body. Yields `NO_BODY` when there is none to read: `204`/`205`, a `HEAD`\n * request, or an empty body under `\"json\"` (e.g. `201 Created` / `202 Accepted` with nothing\n * attached). A body that cannot be parsed fails with `kind: \"decode\"`.\n */\n read: (\n response: Response,\n responseType: ResponseType,\n url: string,\n method?: Method\n ): IO<HttpError, unknown | typeof NO_BODY> =>\n IO.from(readerFor(responseType), () => error(`Unsupported response type: ${responseType}`, url, response)).flatMap(\n (read) =>\n IO.lift(\n async () => {\n if (response.status === 204 || response.status === 205 || method === \"HEAD\") return NO_BODY;\n if (responseType !== \"json\") return read(response);\n return readJson(response);\n },\n (cause) => error(cause, url, response)\n )\n ),\n\n /**\n * Fails with the error for a non-2xx response. The body is read leniently, so an error page\n * that is not JSON (an HTML 502 from a proxy, say) still travels with the error and the message\n * stays the status text: under `\"json\"` the text is parsed when it is JSON and kept as a string\n * otherwise; an unreadable or empty body is `null`.\n */\n readFailure: (response: Response, responseType: ResponseType, url: string): IO<HttpError, never> =>\n IO.promise<unknown>(async () => {\n try {\n if (responseType !== \"json\") {\n return await readerFor(responseType).fold(\n async () => null,\n (read) => read(response)\n );\n }\n // A response without `text()` (a hand-rolled mock) can only be read as JSON.\n if (typeof response.text !== \"function\") return await response.json();\n const text = await response.text();\n if (text.trim() === \"\") return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n } catch {\n return null;\n }\n }).flatMap((body) => IO.fail(failure(response, body, url))),\n};\n","import { HttpError } from \"./http-error\";\n\nconst isAbort = (e: unknown): boolean =>\n typeof e === \"object\" && e !== null && \"name\" in e && (e as { name: unknown }).name === \"AbortError\";\n\n/** @internal */\nexport const HttpTransport = {\n /** An interceptor may reject with a ready-made `HttpError`; it is passed through untouched. */\n error: (e: unknown, url: string): HttpError =>\n e instanceof HttpError ? e : isAbort(e) ? HttpError.aborted(url, e) : HttpError.network(url, e),\n\n /** A timeout means the caller stopped waiting and an abort means someone wanted it stopped: neither is retried. */\n isRetryable: (error: HttpError): boolean =>\n error.kind === \"network\" || (error.kind === \"response\" && error.status >= 500),\n};\n","import { HttpInterceptor, InterceptedRequest } from \"./options\";\n\n/** @internal */\nexport type Send = (request: InterceptedRequest) => Promise<Response>;\n\n/** @internal */\nexport const Interceptors = {\n /** A right fold, so the first registered interceptor is outermost. */\n run: (interceptors: readonly HttpInterceptor[], request: InterceptedRequest, send: Send): Promise<Response> =>\n interceptors.reduceRight<Send>((next, interceptor) => (r) => interceptor.intercept(r, next), send)(request),\n\n /** Builds the request interceptors see: headers normalised to a `Headers` instance. */\n request: (\n url: string,\n init: Omit<RequestInit, \"headers\"> & { headers: Record<string, string> }\n ): InterceptedRequest => ({\n url,\n init: { ...init, headers: new Headers(init.headers) },\n }),\n\n /** The end of the chain. */\n fetch: (request: InterceptedRequest): Promise<Response> => fetch(request.url, request.init),\n};\n","import { List, Stream } from \"@openstage/monadyssey-core\";\nimport type { Pipe } from \"@openstage/monadyssey-core\";\n\n/**\n * A single server-sent event parsed from a text/event-stream response.\n *\n * @property id - The last event id in effect when the event was dispatched — set by the event's\n * own `id:` field or by an earlier one, as the spec's `MessageEvent.lastEventId`. Absent until\n * the server sends an `id:`; the empty string after the server resets it. Used to resume a stream.\n * @property event - The event type. Defaults to `\"message\"` when the server sends no `event:` field.\n * @property data - The event payload. Multiple `data:` lines are joined with a newline.\n * @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.\n */\nexport interface ServerSentEvent {\n readonly id?: string;\n readonly event: string;\n readonly data: string;\n readonly retry?: number;\n}\n\n/**\n * The effects of one block (the lines up to a blank line). A block may set the last event id or\n * the reconnection time without dispatching an event — e.g. a lone `retry: 5000` or `id: 42`.\n *\n * @internal\n */\nexport interface SseFrame {\n /** The event to dispatch, or `null` when the block has no `data:` field. */\n readonly event: ServerSentEvent | null;\n /** The last `id:` value in the block, if any. The empty string resets the last event id. */\n readonly id?: string;\n /** The last valid `retry:` value in the block, if any. */\n readonly retry?: number;\n}\n\nconst parseBlock = (block: string): SseFrame => {\n let event = \"\";\n let id: string | undefined;\n let retry: number | undefined;\n const data: string[] = [];\n\n for (const line of block.split(\"\\n\")) {\n if (line === \"\" || line.startsWith(\":\")) continue; // blank line or comment\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n\n switch (field) {\n case \"event\":\n event = value;\n break;\n case \"data\":\n data.push(value);\n break;\n case \"id\":\n // Per the spec, an id containing NUL is ignored.\n if (!value.includes(\"\\0\")) id = value;\n break;\n case \"retry\":\n if (/^\\d+$/.test(value)) retry = Number.parseInt(value, 10);\n break;\n default:\n break;\n }\n }\n\n return {\n event:\n data.length === 0\n ? null\n : {\n // An empty `event:` field means the default type, not the empty string.\n event: event || \"message\",\n data: data.join(\"\\n\"),\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n },\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n };\n};\n\n/**\n * `event` with `id` set to the last event id in effect (the spec's `lastEventId`), which persists\n * across events until the server changes it.\n *\n * @internal\n */\nexport const withLastEventId = (event: ServerSentEvent, lastEventId: string | undefined): ServerSentEvent => {\n const { id: _blockId, ...rest } = event;\n return lastEventId === undefined ? rest : { ...rest, id: lastEventId };\n};\n\n/**\n * Reassembles a byte stream into complete blocks; partial input and split multi-byte characters\n * are buffered. Call `reset` when a new connection begins.\n *\n * @internal\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n /** The previous chunk ended in CR: a LF opening the next chunk belongs to that CRLF. */\n private skipLeadingLF = false;\n\n push(chunk: Uint8Array): SseFrame[] {\n let text = this.decoder.decode(chunk, { stream: true });\n if (text.length === 0) return [];\n if (this.skipLeadingLF && text.startsWith(\"\\n\")) text = text.slice(1);\n // A trailing CR ends its line now — holding it back until the next chunk would delay every\n // event on CR-only streams by a chunk. If a LF follows, it is skipped above.\n this.skipLeadingLF = text.endsWith(\"\\r\");\n text = this.buffered + text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const blocks = text.split(\"\\n\\n\");\n this.buffered = blocks.pop() ?? \"\";\n return blocks.map(parseBlock);\n }\n\n reset(): void {\n this.decoder = new TextDecoder(\"utf-8\");\n this.buffered = \"\";\n this.skipLeadingLF = false;\n }\n}\n\n/**\n * A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering\n * partial input across chunks. Single-use: each application maintains its own decoding state.\n *\n * @typeParam E - The error type carried by the surrounding stream.\n * @returns A pipe from a byte stream to a stream of parsed events.\n */\nexport const decodeEvents = <E>(): Pipe<E, Uint8Array, ServerSentEvent> => {\n const frames = new FrameBuffer();\n return (bytes: Stream<E, Uint8Array>) =>\n bytes.mapAccumulate<string | undefined, ServerSentEvent>(undefined, (lastEventId, chunk) => {\n const events: ServerSentEvent[] = [];\n let current = lastEventId;\n for (const frame of frames.push(chunk)) {\n if (frame.id !== undefined) current = frame.id;\n if (frame.event !== null) events.push(withLastEventId(frame.event, current));\n }\n return [current, List.from(events)] as const;\n });\n};\n","import { IO, Option, Schedule, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { Credentials, HttpInterceptor } from \"./options\";\nimport { FrameBuffer, ServerSentEvent, SseFrame, withLastEventId } from \"./sse\";\n\n/** A server-sent `retry:` below this is raised to it, so a misbehaving server cannot force a tight loop. */\nconst MIN_SERVER_RETRY_MS = 100;\n\n/** Largest delay `setTimeout` honours; anything above it (or Infinity) fires after ~1ms. */\nconst MAX_DELAY_MS = 2_147_483_647;\n\ntype Reader = ReadableStreamDefaultReader<Uint8Array>;\n\n/** A cancellable sleep. The abort listener is removed when the timer fires, so waits do not accumulate listeners. */\nconst delay = (ms: number): IO<never, void> =>\n IO.promise<void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(\n () => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n },\n Math.min(ms, MAX_DELAY_MS)\n );\n signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/**\n * Carried across reconnections within one run. Owns the one `AbortController` every connection\n * of the run is opened with, linked once to the run's signal; a connection registers a single\n * listener on it and removes it when closed, so listeners stay bounded however long the run lasts.\n */\nclass SseSession {\n readonly controller = new AbortController();\n readonly frames = new FrameBuffer();\n readonly pending: SseFrame[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n /** The server's `retry:` value, once received; it takes precedence over a `Schedule`'s delays. */\n serverRetryMs: number | undefined = undefined;\n /** Consecutive reconnects since the last delivered event. */\n reconnects = 0;\n private connection: { readonly reader: Reader; readonly unlink: () => void } | null = null;\n private readonly unlinkParent: () => void;\n\n constructor(retryMs: number, parent: AbortSignal) {\n this.retryMs = retryMs;\n const onAbort = () => this.controller.abort(parent.reason);\n parent.addEventListener(\"abort\", onAbort, { once: true });\n this.unlinkParent = () => parent.removeEventListener(\"abort\", onAbort);\n }\n\n get reader(): Reader | null {\n return this.connection?.reader ?? null;\n }\n\n attach(reader: Reader): void {\n const onAbort = () => reader.cancel().catch(() => {});\n this.controller.signal.addEventListener(\"abort\", onAbort, { once: true });\n this.connection = { reader, unlink: () => this.controller.signal.removeEventListener(\"abort\", onAbort) };\n this.frames.reset();\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.unlink();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n this.unlinkParent();\n this.controller.abort();\n }\n}\n\n/** @internal */\nexport interface SseConfig {\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly defaultHeaders: Record<string, string>;\n readonly credentials: Credentials;\n readonly interceptors: readonly HttpInterceptor[];\n readonly reconnect: boolean | Schedule;\n readonly retry: number;\n}\n\nconst mediaType = (contentType: string): string => contentType.split(\";\")[0].trim().toLowerCase();\n\nconst stream = (config: SseConfig): Stream<HttpError, ServerSentEvent> => {\n const { url, headers, defaultHeaders, credentials, interceptors, reconnect, retry } = config;\n\n /** The reader for a new connection, or `null` when the server answered 204 (\"stop reconnecting\"). */\n const open = (session: SseSession): IO<HttpError, Reader | null> => {\n const prepared = HttpRequest.prepare(\n \"GET\",\n {\n headers: {\n ...headers,\n accept: \"text/event-stream\",\n // An empty id is how a server resets it: the header is sent only for a non-empty id.\n ...(session.lastEventId ? { \"last-event-id\": session.lastEventId } : {}),\n },\n credentials,\n },\n { headers: defaultHeaders, credentials }\n );\n return IO.lift<HttpError, Response>(\n () =>\n Interceptors.run(\n interceptors,\n Interceptors.request(url, { ...prepared, signal: session.controller.signal }),\n Interceptors.fetch\n ),\n (e: unknown) => HttpTransport.error(e, url)\n )\n .flatMap((response) => (response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"text\", url)))\n .flatMap((response): IO<HttpError, Reader | null> => {\n if (response.status === 204) {\n void response.body?.cancel().catch(() => {});\n return IO.of(null);\n }\n // Not an event stream (e.g. an HTML login page or SPA fallback): fail without\n // reconnecting, instead of parsing nothing and reconnecting forever. A missing\n // content type is tolerated.\n const contentType = response.headers.get(\"content-type\");\n if (contentType !== null && mediaType(contentType) !== \"text/event-stream\") {\n void response.body?.cancel().catch(() => {});\n return IO.fail(HttpResponse.error(`Expected text/event-stream, got ${contentType}`, url, response));\n }\n return response.body === null\n ? IO.fail<HttpError, Reader>(HttpResponse.error(\"Response has no body\", url, response))\n : IO.of(response.body.getReader());\n });\n };\n\n const readChunk = (reader: Reader): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.lift(\n () => reader.read(),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\n const done = IO.of<EventStep>(Option.empty());\n\n /**\n * Reconnects after the scheduled delay, or gives up (`giveUp`) when reconnection is off or the\n * `Schedule` allows no further attempt. A server `retry:` value, once received, is the delay.\n */\n const reconnectOr = (session: SseSession, giveUp: IO<HttpError, EventStep>): IO<HttpError, EventStep> => {\n if (reconnect === false) return giveUp;\n const made = session.reconnects + 1;\n if (reconnect !== true && !reconnect.allowsAttempt(made)) return giveUp;\n const ms = session.serverRetryMs ?? (reconnect === true ? session.retryMs : reconnect.delayAfter(made));\n session.reconnects = made;\n return delay(ms).flatMap(() => step(session));\n };\n\n const step = (session: SseSession): IO<HttpError, EventStep> => {\n while (session.pending.length > 0) {\n const frame = session.pending.shift() as SseFrame;\n // `id:` and `retry:` take effect even in a block that dispatches no event.\n if (frame.id !== undefined) session.lastEventId = frame.id;\n if (frame.retry !== undefined) session.serverRetryMs = Math.max(frame.retry, MIN_SERVER_RETRY_MS);\n if (frame.event !== null) {\n session.reconnects = 0;\n return IO.of(\n Option.of<[ServerSentEvent, SseSession]>([withLastEventId(frame.event, session.lastEventId), session])\n );\n }\n }\n\n const reader = session.reader;\n if (reader === null) {\n return open(session).foldM<HttpError, EventStep>(\n (error) => (HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error)),\n (opened) => {\n if (opened === null) return done;\n session.attach(opened);\n return step(session);\n }\n );\n }\n\n return readChunk(reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n return HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n return reconnectOr(session, done);\n }\n session.pending.push(...session.frames.push(result.value));\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.promise(async (signal: AbortSignal) => new SseSession(retry, signal)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.sync(() => session.close())\n );\n};\n\n/** @internal */\nexport const SseClient = { stream };\n","import { IO, Option, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest, PreparedRequest } from \"./http-request\";\nimport { HttpResponse, NO_BODY } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { SseClient } from \"./sse-client\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, SendOptions, SseOptions } from \"./options\";\nimport { ServerSentEvent } from \"./sse\";\n\n/** Options for a request that expects no body back, or `Option` of one. */\ntype OptionalBody<A> = Options<A> & { empty: \"option\" };\n/** Options for a request whose body is required (the default). */\ntype RequiredBody<A> = Options<A> & { empty?: \"error\" };\n\n/**\n * A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of\n * Promises. Each instance carries its own configuration (base URL, interceptors, default headers,\n * timeout, credentials), so different parts of an application can use independently configured\n * clients.\n *\n * Three kinds of call, by what you want back:\n * - **the decoded body** — `get`, `post`, `put`, `patch`, `delete`, `fetch` return\n * `IO<HttpError, A>`. A response with no body (`204`, an empty `200`) fails with\n * `kind: \"decode\"`, since a value was expected; pass `{ empty: \"option\" }` when absence is\n * legitimate, to get `IO<HttpError, Option<A>>`.\n * - **nothing** — `send(method, uri, { body })` returns `IO<HttpError, void>` for actions whose\n * response body you do not read (publish, archive, leave …); any body, or none, is fine.\n * - **the `Response`** — `raw(method, uri, options?)` returns `IO<HttpError, Response>` for\n * status codes, headers or streaming.\n *\n * Every non-2xx response fails with `kind: \"response\"`, whichever you use. Cancellation is\n * supported: when the IO is cancelled (via a fiber or `timeout`), the underlying `fetch` is\n * aborted through its `AbortSignal`.\n *\n * `A` is what you claim the body is; the JSON is not checked unless you pass a `transform` that\n * validates it (a schema's `parse`, say) — a throw there fails with `kind: \"decode\"`.\n *\n * **Default credentials:** `same-origin`, matching the platform `fetch` default. Set\n * `credentials: \"include\"` explicitly per-client or per-request if you need cookies to flow\n * cross-origin.\n *\n * @example\n * const api = new HttpClient({\n * baseUrl: \"https://api.example.com\",\n * interceptors: [authInterceptor],\n * timeout: 5000,\n * });\n *\n * const users = api.get<User[]>(\"/users\"); // IO<HttpError, User[]>\n * const avatar = api.get<Avatar>(\"/me/avatar\", { empty: \"option\" }); // IO<HttpError, Option<Avatar>>\n * const leave = api.send(\"POST\", `/communities/${slug}/leave`); // IO<HttpError, void>\n */\nexport class HttpClient {\n private readonly interceptors: readonly HttpInterceptor[];\n private readonly baseUrl: string | undefined;\n private readonly defaultHeaders: Record<string, string>;\n private readonly defaultTimeout: number | undefined;\n private readonly defaultCredentials: Credentials;\n\n /**\n * Creates a new HttpClient with the given configuration.\n *\n * @param {HttpClientConfig} config - Configuration for the client.\n */\n constructor(config: HttpClientConfig = {}) {\n this.interceptors = Object.freeze([...(config.interceptors ?? [])]);\n this.baseUrl = config.baseUrl;\n this.defaultHeaders = config.defaultHeaders ?? {};\n this.defaultTimeout = config.timeout;\n this.defaultCredentials = config.credentials ?? \"same-origin\";\n }\n\n /**\n * Performs a GET request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n get<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n get<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n get<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"GET\", options);\n }\n\n /**\n * Performs a POST request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n post<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n post<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n post<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"POST\", { ...options, body });\n }\n\n /**\n * Performs a PUT request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n put<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n put<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n put<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PUT\", { ...options, body });\n }\n\n /**\n * Performs a PATCH request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n patch<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n patch<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n patch<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PATCH\", { ...options, body });\n }\n\n /**\n * Performs a DELETE request and decodes the body. For a DELETE that answers `204 No Content`,\n * use `send(\"DELETE\", uri)` instead.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n delete<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n delete<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n delete<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"DELETE\", options);\n }\n\n /**\n * Performs a request with any method and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Method} method - The HTTP method.\n * @param {Options<A>} [options] - Request options, including `body`; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n fetch<A = unknown>(uri: string, method: Method, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n fetch<A = unknown>(uri: string, method: Method, options?: RequiredBody<A>): IO<HttpError, A>;\n fetch<A = unknown>(uri: string, method: Method, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, method, options);\n }\n\n /**\n * Performs a request whose response body you do not need — an action such as publish,\n * archive or leave. Succeeds with `void` for any 2xx, with or without a body (which is\n * discarded); a non-2xx response fails with `kind: \"response\"` and its body.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body. A `GET` or `HEAD`\n * request with a body throws `TypeError` where it is built.\n * @returns {IO<HttpError, void>}\n *\n * @example\n * client.send(\"POST\", `/api/v1/communities/${slug}/leave`);\n * client.send(\"PUT\", `/api/v1/articles/${id}`, { body: article });\n * client.send(\"DELETE\", `/api/v1/passkeys/${id}`, { headers: { \"X-Step-Up\": token } });\n */\n send(method: Method, uri: string, options: SendOptions = {}): IO<HttpError, void> {\n const url = this.resolveUrl(uri);\n return this.withTimeout(\n url,\n options.timeout,\n this.exchange(url, method, options).flatMap((response) =>\n response.ok\n ? IO.sync(() => void response.body?.cancel().catch(() => {}))\n : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Performs a request and yields the `Response` itself, for status codes, headers or streaming.\n * A non-2xx response still fails with `kind: \"response\"` (its body read into the error); a 2xx\n * `Response` is returned unread. When you only need the status or headers, cancel the body\n * (`response.body?.cancel()`) so the connection is released; `send` does this for you.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body.\n * @returns {IO<HttpError, Response>}\n *\n * @example\n * client.raw(\"POST\", \"/api/v1/communities\", { body: input }).map((r) => r.headers.get(\"Location\"));\n */\n raw(method: Method, uri: string, options: SendOptions = {}): IO<HttpError, Response> {\n const url = this.resolveUrl(uri);\n return this.withTimeout(\n url,\n options.timeout,\n this.exchange(url, method, options).flatMap((response) =>\n response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.\n *\n * The request goes through the same pipeline as every other method, so headers (including\n * `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is\n * used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts\n * the connection and releases resources.\n *\n * By default the stream reconnects automatically when the connection ends or fails with a\n * retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers\n * and credentials on each attempt; terminal `4xx` responses (including auth failures) end the\n * stream. See {@link SseOptions} to configure or disable this.\n *\n * @param {string} uri - The URL or path to connect to.\n * @param {SseOptions} [options] - Headers, credentials, and reconnection settings.\n * @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.\n * @throws {RangeError} If `options.retry` is negative or `NaN`.\n *\n * @example\n * const events = client.sse(\"/events\", { headers: { Authorization: `Bearer ${token}` } });\n * await events.runForEach((event) => IO.sync(() => handle(event))).unsafeRun();\n */\n sse(uri: string, options: SseOptions = {}): Stream<HttpError, ServerSentEvent> {\n // A negative or NaN delay is a programming error; left through, `setTimeout` would treat it\n // as 0 and reconnect in a tight loop.\n if (options.retry !== undefined && !(options.retry >= 0)) {\n throw new RangeError(`HttpClient.sse: retry must be a non-negative number of milliseconds, got ${options.retry}`);\n }\n return SseClient.stream({\n url: this.resolveUrl(uri),\n headers: options.headers ?? {},\n defaultHeaders: this.defaultHeaders,\n credentials: options.credentials ?? this.defaultCredentials,\n interceptors: this.interceptors,\n reconnect: options.reconnect ?? true,\n retry: options.retry ?? 3000,\n });\n }\n\n private resolveUrl(uri: string): string {\n // Absolute only with a scheme followed by `//` (or a data:/blob: URL). `new URL()` also\n // accepts `items:batchGet` or `localhost:3000` as absolute, which would bypass `baseUrl`.\n if (!this.baseUrl || /^[a-z][a-z\\d+.-]*:\\/\\//i.test(uri) || /^(data|blob):/i.test(uri)) return uri;\n const base = this.baseUrl.endsWith(\"/\") ? this.baseUrl.slice(0, -1) : this.baseUrl;\n const path = uri.startsWith(\"/\") ? uri : `/${uri}`;\n return `${base}${path}`;\n }\n\n /** Prepares the request, runs it through the interceptors and `fetch`, and yields the `Response`. */\n private exchange(url: string, method: Method, options: SendOptions): IO<HttpError, Response> {\n if ((method === \"GET\" || method === \"HEAD\") && options.body != null) {\n // A programming error, so it throws where the request is built instead of being dropped.\n throw new TypeError(`A ${method} request cannot have a body (${url})`);\n }\n const defaults = { headers: this.defaultHeaders, credentials: this.defaultCredentials };\n const prepare: IO<HttpError, PreparedRequest> = IO.lift(\n () => HttpRequest.prepare(method, options, defaults),\n (e: unknown) => HttpError.request(url, e)\n );\n return prepare.flatMap((prepared) =>\n IO.cancellable(\n (signal: AbortSignal) =>\n Interceptors.run(this.interceptors, Interceptors.request(url, { ...prepared, signal }), Interceptors.fetch),\n (e: unknown) => HttpTransport.error(e, url)\n )\n );\n }\n\n private withTimeout<A>(url: string, timeout: number | undefined, io: IO<HttpError, A>): IO<HttpError, A> {\n const ms = timeout ?? this.defaultTimeout;\n return ms == null ? io : io.timeout(ms, () => HttpError.timeout(url, ms));\n }\n\n private request<A>(uri: string, method: Method, options: Options<A>): IO<HttpError, A | Option<NonNullable<A>>> {\n const url = this.resolveUrl(uri);\n const { responseType = \"json\", transform = (data: unknown) => data as A, empty = \"error\" } = options;\n\n const decode = (response: Response): IO<HttpError, A | Option<NonNullable<A>>> =>\n response.ok\n ? HttpResponse.read(response, responseType, url, method).flatMap((parsed) => {\n if (parsed === NO_BODY) {\n return empty === \"option\"\n ? IO.of(Option.empty<NonNullable<A>>())\n : IO.fail(\n HttpError.decode(\n url,\n response,\n `expected a response body, got none (status ${response.status}); use send() or { empty: \"option\" }`\n )\n );\n }\n return IO.lift<HttpError, A>(\n () => transform(parsed),\n (e: unknown) => HttpError.decode(url, response, e)\n ).map((value) => (empty === \"option\" ? Option.ofNullable(value) : value));\n })\n : HttpResponse.readFailure(response, responseType, url);\n\n return this.withTimeout(url, options.timeout, this.exchange(url, method, options).flatMap(decode));\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openstage/monadyssey-fetch",
|
|
3
|
-
"version": "3.0.0-
|
|
3
|
+
"version": "3.0.0-rc.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -18,27 +18,31 @@
|
|
|
18
18
|
],
|
|
19
19
|
"author": "Gabriel Bornea",
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"LICENSE"
|
|
22
23
|
],
|
|
23
|
-
"
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.19"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/monadyssey-fetch.mjs",
|
|
24
29
|
"module": "./dist/monadyssey-fetch.mjs",
|
|
25
|
-
"
|
|
30
|
+
"types": "./dist/monadyssey-fetch.d.ts",
|
|
26
31
|
"exports": {
|
|
27
32
|
".": {
|
|
28
33
|
"types": "./dist/monadyssey-fetch.d.ts",
|
|
29
|
-
"
|
|
30
|
-
"require": "./dist/monadyssey-fetch.cjs"
|
|
34
|
+
"default": "./dist/monadyssey-fetch.mjs"
|
|
31
35
|
},
|
|
32
36
|
"./package.json": "./package.json"
|
|
33
37
|
},
|
|
34
38
|
"scripts": {
|
|
35
39
|
"test": "npx jest --verbose --coverage --config ../../jest.config.cjs --detectOpenHandles",
|
|
36
|
-
"build": "
|
|
40
|
+
"build": "tsc --noEmit -p tsconfig.json && vite build --config vite.config.ts && tsc -p tsconfig.build.json && api-extractor run --local --config api-extractor.json && node -e \"require('node:fs').rmSync('dist/types', { recursive: true, force: true })\"",
|
|
37
41
|
"format": "npm run prettier:fix",
|
|
38
42
|
"prettier": "npx prettier \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" --check",
|
|
39
43
|
"prettier:fix": "npm run prettier -- --write"
|
|
40
44
|
},
|
|
41
45
|
"peerDependencies": {
|
|
42
|
-
"@openstage/monadyssey-core": "^3.0.0-
|
|
46
|
+
"@openstage/monadyssey-core": "^3.0.0-rc.1"
|
|
43
47
|
}
|
|
44
48
|
}
|
package/readme.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/@openstage/monadyssey-fetch)
|
|
2
|
-
[](
|
|
2
|
+
[](https://gitlab.com/open-stage/monadyssey/-/tree/main/docs/monadyssey-fetch)
|
|
3
3
|
|
|
4
4
|
### Overview
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ error handling, declarative workflows, and type safety when interacting with API
|
|
|
11
11
|
|
|
12
12
|
Explore the documentation for specific features:
|
|
13
13
|
|
|
14
|
-
- [HttpClient](
|
|
14
|
+
- [HttpClient](https://gitlab.com/open-stage/monadyssey/-/blob/main/docs/monadyssey-fetch/http-client.md): Composable HTTP client with base URL, interceptors, cancellation, timeout, native body type support, and server-sent events (with automatic reconnection).
|
|
15
15
|
|
|
16
16
|
### Installation
|
|
17
17
|
|
|
@@ -21,6 +21,10 @@ To use `@openstage/monadyssey-fetch` in your project, install it via npm:
|
|
|
21
21
|
npm install @openstage/monadyssey-fetch @openstage/monadyssey-core
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
The 3.0.0 release candidate is published under the `rc` tag: `npm install @openstage/monadyssey-fetch@rc @openstage/monadyssey-core@rc`.
|
|
25
|
+
|
|
26
|
+
**Requirements:** an ES module environment (bundler, or Node ≥ 20.19) and TypeScript ≥ 5.0 for the type declarations (they use `const` type parameters). The declarations use the platform `fetch` types (`Response`, `Headers`, `RequestInit`, `AbortSignal`), so a TypeScript project needs them in scope: `"lib": ["dom"]` in a browser app, or `@types/node` (Node ≥ 18 declares them globally). The library itself is built and type-checked with TypeScript 7.
|
|
27
|
+
|
|
24
28
|
### Features
|
|
25
29
|
|
|
26
30
|
#### Instance-Based Configuration
|
|
@@ -48,6 +52,20 @@ boundary automatically.
|
|
|
48
52
|
Interceptors are passed at construction time and are immutable. They can transform requests, short-circuit responses,
|
|
49
53
|
retry on failure, or modify responses. Different client instances can have independent interceptor stacks.
|
|
50
54
|
|
|
55
|
+
#### Three kinds of call
|
|
56
|
+
|
|
57
|
+
- `get` / `post` / `put` / `patch` / `delete` / `fetch` decode the body: `IO<HttpError, T>`. A response without a body
|
|
58
|
+
(`204`, an empty `200`) fails with `kind: "decode"`, since a value was expected; pass `{ empty: "option" }` to get
|
|
59
|
+
`IO<HttpError, Option<T>>` when absence is legitimate.
|
|
60
|
+
- `send(method, uri, { body })` is for actions whose response body you do not read: `IO<HttpError, void>`, any 2xx.
|
|
61
|
+
- `raw(method, uri, options?)` returns the `Response` itself: `IO<HttpError, Response>`.
|
|
62
|
+
|
|
63
|
+
#### Server-Sent Events
|
|
64
|
+
|
|
65
|
+
`client.sse(uri, options?)` returns a `Stream<HttpError, ServerSentEvent>` over a fetch-based connection, so
|
|
66
|
+
`Authorization` headers and cookies are sent, with automatic reconnection (optionally on a `Schedule`) that resumes
|
|
67
|
+
from `Last-Event-ID`. `decodeEvents()` is the same parser as a `Pipe` for your own byte streams.
|
|
68
|
+
|
|
51
69
|
#### Explicit Error Handling
|
|
52
70
|
|
|
53
71
|
All requests return `IO<HttpError, T>`, explicitly modeling success and failure. Errors include HTTP status, response
|
|
@@ -85,7 +103,8 @@ await api.post<User>("/users", { name: "New User" })
|
|
|
85
103
|
const formData = new FormData();
|
|
86
104
|
formData.append("file", file);
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
// send(): the upload's response body is not needed (and may be empty)
|
|
107
|
+
await api.send("POST", "/upload", { body: formData }).unsafeRun();
|
|
89
108
|
```
|
|
90
109
|
|
|
91
110
|
**Custom Request with `fetch`**
|
|
@@ -103,21 +122,21 @@ await api.fetch<{ message: string }>("/custom", "OPTIONS", {
|
|
|
103
122
|
| `body` | `unknown` | Request payload. Objects auto-JSON; FormData/Blob passed as-is |
|
|
104
123
|
| `responseType`| `"json"`, `"text"`, `"blob"`, `"arrayBuffer"`, `"formData"` | Expected response type. **Defaults to `"json"`** |
|
|
105
124
|
| `credentials` | `"omit"`, `"same-origin"`, `"include"` | Credential policy. **Defaults to client setting (`"same-origin"`)** |
|
|
106
|
-
| `
|
|
107
|
-
| `transform` | `(data: unknown) => A` |
|
|
125
|
+
| `empty` | `"error"` or `"option"` | A response without a body: fail with `"decode"`, or yield `Option<A>`. **Defaults to `"error"`** |
|
|
126
|
+
| `transform` | `(data: unknown) => A` | Decode or validate the body; only called when there is one |
|
|
108
127
|
| `timeout` | `number` | Per-request timeout in milliseconds. Overrides client-level timeout |
|
|
109
128
|
|
|
110
129
|
### Error Handling
|
|
111
130
|
|
|
112
131
|
Errors are encapsulated in the `HttpError` type, which includes:
|
|
113
132
|
|
|
114
|
-
* `kind`: The transport outcome — `"request"` when the request could not be built and nothing was sent, `"response"` when the server answered, `"network"` when nothing
|
|
115
|
-
* `status`: The HTTP status code, or `0` when no response arrived
|
|
133
|
+
* `kind`: The transport outcome — `"request"` when the request could not be built and nothing was sent, `"response"` when the server answered with a non-2xx status, `"decode"` when it answered 2xx but the body could not be used (unparseable, rejected by `transform`, or missing), `"network"` when nothing answered, `"timeout"` when the `timeout` option fired, `"aborted"` when the request was aborted outside IO cancellation
|
|
134
|
+
* `status`: The HTTP status code, or `0` when no response arrived (it is the real status for `"response"` and `"decode"`)
|
|
116
135
|
* `message`: Formatted error message containing the URL and, for a response, the status and raw message
|
|
117
136
|
* `cause`: The underlying exception, if any
|
|
118
137
|
* `timeoutMs`: The timeout that fired, in milliseconds; set only when `kind` is `"timeout"`
|
|
119
138
|
* `rawMessage`: The raw error description
|
|
120
|
-
* `body`: The response body if available, typed `unknown`. Narrow it safely with `error.bodyAs(guard)`, which returns `Option<T
|
|
139
|
+
* `body`: The response body if available, typed `unknown`. Narrow it safely with `error.bodyAs(guard)`, which returns `Option<NonNullable<T>>`
|
|
121
140
|
* `url`: The request URL
|
|
122
141
|
* `headers`: The response headers if available
|
|
123
142
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@openstage/monadyssey-core");var t={describe:e=>e instanceof Error?e.message:typeof e==`string`?e:typeof e==`object`&&e&&`message`in e?String(e.message):`An unknown error occurred.`,format:(e,t,n,r,i)=>{switch(e){case`response`:return`Request to '${t}' failed with status ${n} and message: ${r}.`;case`request`:return`Request to '${t}' could not be sent: ${r}.`;case`timeout`:return`Request to '${t}' timed out after ${i} ms.`;case`aborted`:return`Request to '${t}' was aborted before a response arrived: ${r}.`;default:return`Request to '${t}' failed before a response arrived: ${r}.`}}},n=class n extends Error{kind;status;rawMessage;body;url;headers;cause;timeoutMs;constructor(e,n,r,i,a,o=`response`,s,c){super(t.format(o,i,e,n,c)),this.name=`HttpError`,this.kind=o,this.status=e,this.rawMessage=n,this.body=r,this.url=i,this.headers=a,this.cause=s,this.timeoutMs=c}static network(e,r){return new n(0,t.describe(r),null,e,void 0,`network`,r)}static timeout(e,t){return new n(0,`Request timed out after ${t} ms`,null,e,void 0,`timeout`,void 0,t)}static request(e,r){return new n(0,t.describe(r),null,e,void 0,`request`,r)}static aborted(e,r){return new n(0,t.describe(r),null,e,void 0,`aborted`,r)}bodyAs(t){return t(this.body)?e.Some.of(this.body):e.Option.empty()}},r=e=>typeof e!=`object`||!e||e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||e instanceof URLSearchParams||typeof ReadableStream<`u`&&e instanceof ReadableStream||ArrayBuffer.isView(e),i=e=>Object.fromEntries(Object.entries(e).filter(e=>e[1]!==void 0).map(([e,t])=>[e.toLowerCase(),t])),a=(e,t)=>{if(t instanceof FormData){let{"content-type":t,...n}=e;return n}return typeof t==`object`&&t&&!r(t)&&!(`content-type`in e)?{...e,"content-type":`application/json`}:e},o=(e,t,n)=>e===`GET`||e===`HEAD`||t==null?void 0:r(t)?t:n[`content-type`]===`application/json`?JSON.stringify(t):t,s={isNativeBody:r,normalizeHeaders:i,prepare:(e,t,n)=>{let r=a(i({...n.headers,...t.headers??{}}),t.body);return{method:e,headers:r,credentials:t.credentials??n.credentials,body:o(e,t.body,r)}}},c={json:e=>e.json(),text:e=>e.text(),blob:e=>e.blob(),arrayBuffer:e=>e.arrayBuffer(),formData:e=>e.formData()},l=t=>Object.prototype.hasOwnProperty.call(c,t)?e.Option.of(c[t]):e.Option.empty(),u=e=>{try{return Object.fromEntries(e.headers.entries())}catch{return{}}},d=(e,r,i)=>new n(i.status,t.describe(e),null,r,u(i),`response`,e),f=(e,t,r)=>new n(e.status,e.statusText,t,r,u(e)),p={headers:u,error:d,failure:f,read:(t,n,r)=>e.IO.from(l(n),()=>d(`Unsupported response type: ${n}`,r,t)).flatMap(n=>e.IO.lift(()=>t.status===204||t.status===205?null:n(t),e=>d(e,r,t))),settle:(t,n,r)=>t.ok?e.IO.of(n):e.IO.fail(f(t,n,r))},m=e=>typeof e==`object`&&!!e&&`name`in e&&e.name===`AbortError`,h={error:(e,t)=>e instanceof n?e:m(e)?n.aborted(t,e):n.network(t,e),isRetryable:e=>e.kind===`network`||e.kind===`response`&&e.status>=500},g={run:(e,t,n)=>e.reduceRight((e,t)=>n=>t.intercept(n,e),n)(t)},_=e=>{let t,n,r,i=[];for(let a of e.split(`
|
|
2
|
-
`)){if(a===``||a.startsWith(`:`))continue;let e=a.indexOf(`:`),o=e===-1?a:a.slice(0,e),s=e===-1?``:a.slice(e+1);switch(s.startsWith(` `)&&(s=s.slice(1)),o){case`event`:t=s;break;case`data`:i.push(s);break;case`id`:n=s;break;case`retry`:/^\d+$/.test(s)&&(r=Number.parseInt(s,10))}}return i.length===0?null:{event:t??`message`,data:i.join(`
|
|
3
|
-
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},v=class{decoder=new TextDecoder(`utf-8`);buffered=``;push(e){let t=this.buffered+this.decoder.decode(e,{stream:!0}),n=!1;t.endsWith(`\r`)&&(t=t.slice(0,-1),n=!0),t=t.replace(/\r\n/g,`
|
|
4
|
-
`).replace(/\r/g,`
|
|
5
|
-
`);let r=t.split(`
|
|
6
|
-
|
|
7
|
-
`),i=r.pop()??``;return n&&(i+=`\r`),this.buffered=i,r.map(_).filter(e=>e!==null)}reset(){this.decoder=new TextDecoder(`utf-8`),this.buffered=``}},y=()=>{let t=new v;return n=>n.mapAccumulate(null,n=>[null,e.List.from(t.push(n))])},b=100,x=t=>e.IO.cancellable(e=>new Promise(n=>{let r=()=>{clearTimeout(i),n()},i=setTimeout(()=>{e.removeEventListener(`abort`,r),n()},t);e.addEventListener(`abort`,r,{once:!0})})),S=class{controller=new AbortController;frames=new v;pending=[];lastEventId=void 0;retryMs;connection=null;unlinkParent;constructor(e,t){this.retryMs=e;let n=()=>this.controller.abort(t.reason);t.addEventListener(`abort`,n,{once:!0}),this.unlinkParent=()=>t.removeEventListener(`abort`,n)}get reader(){return this.connection?.reader??null}attach(e){let t=()=>e.cancel().catch(()=>{});this.controller.signal.addEventListener(`abort`,t,{once:!0}),this.connection={reader:e,unlink:()=>this.controller.signal.removeEventListener(`abort`,t)},this.frames.reset()}closeConnection(){this.connection&&=(this.connection.unlink(),this.connection.reader.cancel().catch(()=>{}),null)}close(){this.closeConnection(),this.unlinkParent(),this.controller.abort()}},C={stream:t=>{let{url:n,headers:r,defaultHeaders:i,credentials:a,interceptors:o,reconnect:c,retry:l}=t,u=t=>{let c=s.prepare(`GET`,{headers:{...r,accept:`text/event-stream`,...t.lastEventId===void 0?{}:{"last-event-id":t.lastEventId}},credentials:a},{headers:i,credentials:a});return e.IO.lift(()=>g.run(o,{...c,signal:t.controller.signal},e=>fetch(n,e)),e=>h.error(e,n)).flatMap(t=>t.ok?e.IO.of(t):p.read(t,`text`,n).flatMapErr(()=>e.IO.of(null)).flatMap(r=>e.IO.fail(p.failure(t,r,n)))).flatMap(t=>t.body===null?e.IO.fail(p.error(`Response has no body`,n,t)):e.IO.of(t.body.getReader()))},d=t=>e.IO.lift(()=>t.read(),e=>h.error(e,n)),f=e.IO.of(e.Option.empty()),m=e=>x(e.retryMs).flatMap(()=>_(e)),_=t=>{if(t.pending.length>0){let n=t.pending.shift();return n.id!==void 0&&(t.lastEventId=n.id),n.retry!==void 0&&(t.retryMs=Math.max(n.retry,b)),e.IO.of(e.Option.of([n,t]))}let n=t.reader;return n===null?u(t).foldM(n=>c&&h.isRetryable(n)?m(t):e.IO.fail(n),e=>(t.attach(e),_(t))):d(n).foldM(n=>(t.closeConnection(),c&&h.isRetryable(n)?m(t):e.IO.fail(n)),e=>e.done?(t.closeConnection(),c?m(t):f):(t.pending.push(...t.frames.push(e.value)),_(t)))};return e.Stream.bracket(e.IO.cancellable(e=>new S(l,e)),t=>e.Stream.unfoldEval(t,_),t=>e.IO.lift(()=>t.close()))}},w=class{interceptors;baseUrl;defaultHeaders;defaultTimeout;defaultCredentials;constructor(e={}){this.interceptors=Object.freeze([...e.interceptors??[]]),this.baseUrl=e.baseUrl,this.defaultHeaders=e.defaultHeaders??{},this.defaultTimeout=e.timeout,this.defaultCredentials=e.credentials??`same-origin`}get(e,t){return this.request(e,`GET`,t)}post(e,t,n){return this.request(e,`POST`,{...n,body:t})}put(e,t,n){return this.request(e,`PUT`,{...n,body:t})}patch(e,t,n){return this.request(e,`PATCH`,{...n,body:t})}delete(e,t){return this.request(e,`DELETE`,t)}fetch(e,t,n={}){return this.request(e,t,n)}sse(e,t={}){return C.stream({url:this.resolveUrl(e),headers:t.headers??{},defaultHeaders:this.defaultHeaders,credentials:t.credentials??this.defaultCredentials,interceptors:this.interceptors,reconnect:t.reconnect??!0,retry:t.retry??3e3})}resolveUrl(e){if(!this.baseUrl)return e;try{return new URL(e),e}catch{return`${this.baseUrl.endsWith(`/`)?this.baseUrl.slice(0,-1):this.baseUrl}${e.startsWith(`/`)?e:`/${e}`}`}}request(t,r,i={}){let a=this.resolveUrl(t),o=i.timeout??this.defaultTimeout,{responseType:c=`json`,observe:l=`body`,transform:u=e=>e}=i,d={headers:this.defaultHeaders,credentials:this.defaultCredentials},f=e.IO.lift(()=>s.prepare(r,i,d),e=>n.request(a,e)),m=t=>e.IO.cancellable(e=>g.run(this.interceptors,{...t,signal:e},e=>fetch(a,e)),e=>h.error(e,a)),_=t=>p.read(t,c,a).flatMap(e=>p.settle(t,e,a)).flatMap(n=>e.IO.lift(()=>u(n),e=>p.error(e,a,t))),v=f.flatMap(m).flatMap(t=>t.ok&&l===`response`?e.IO.of(t):_(t));return o==null?v:v.timeout(o,()=>n.timeout(a,o))}};exports.HttpClient=w,exports.HttpError=n,exports.decodeEvents=y;
|
|
8
|
-
//# sourceMappingURL=monadyssey-fetch.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.cjs","names":[],"sources":["../src/http-error.ts","../src/http-request.ts","../src/http-response.ts","../src/http-transport.ts","../src/interceptors.ts","../src/sse.ts","../src/sse-client.ts","../src/http-client.ts"],"sourcesContent":["import { Option, Some } from \"@openstage/monadyssey-core\";\n\n/**\n * How a request failed.\n * - `\"request\"`: the request could not be built — typically the body could not be serialized.\n * Nothing was sent.\n * - `\"response\"`: the server answered with a non-2xx status, or a 2xx whose body could not be parsed.\n * - `\"network\"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.\n * - `\"timeout\"`: the client's `timeout` option fired before the response, or its body, completed.\n * The request may have reached the server.\n * - `\"aborted\"`: the request was aborted by something other than IO cancellation, such as an\n * interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as\n * `Cancelled`, not as an `HttpError`).\n */\nexport type HttpErrorKind = \"request\" | \"response\" | \"network\" | \"timeout\" | \"aborted\";\n\n/** @internal */\nexport const HttpErrorMessage = {\n describe: (cause: unknown): string =>\n cause instanceof Error\n ? cause.message\n : typeof cause === \"string\"\n ? cause\n : typeof cause === \"object\" && cause !== null && \"message\" in cause\n ? String((cause as { message: unknown }).message)\n : \"An unknown error occurred.\",\n\n /** Response errors keep the pre-3.0 wording; the other kinds carry no status. */\n format: (kind: HttpErrorKind, url: string, status: number, rawMessage: string, timeoutMs?: number): string => {\n switch (kind) {\n case \"response\":\n return `Request to '${url}' failed with status ${status} and message: ${rawMessage}.`;\n case \"request\":\n return `Request to '${url}' could not be sent: ${rawMessage}.`;\n case \"timeout\":\n return `Request to '${url}' timed out after ${timeoutMs} ms.`;\n case \"aborted\":\n return `Request to '${url}' was aborted before a response arrived: ${rawMessage}.`;\n default:\n return `Request to '${url}' failed before a response arrived: ${rawMessage}.`;\n }\n },\n};\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP\n * status code, raw error message, response body, request URL, response headers, and the\n * underlying cause.\n *\n * `kind` tells the two facts apart that `status` alone cannot: \"the server said 500\" is\n * `kind: \"response\", status: 500`, while \"nothing answered\" is `kind: \"network\", status: 0`.\n * For every kind other than `\"response\"`, `status` is `0` and `body` is `null`.\n *\n * @example\n * client.post(\"/payments\", payment).mapErr((error) =>\n * error.kind === \"response\"\n * ? { kind: \"rejected\", status: error.status }\n * : { kind: \"outcomeUnknown\" } // the request may or may not have reached the server\n * );\n */\nexport class HttpError extends Error {\n /** The transport outcome. See {@link HttpErrorKind}. */\n public readonly kind: HttpErrorKind;\n /** The HTTP status code, or `0` when no response arrived (`kind !== \"response\"`). */\n public readonly status: number;\n public readonly rawMessage: string;\n /** The parsed response body, or `null` when none was parsed. */\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n /** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */\n public readonly cause?: unknown;\n /** The timeout that fired, in milliseconds. Set only when `kind === \"timeout\"`. */\n public readonly timeoutMs?: number;\n\n constructor(\n status: number,\n rawMessage: string,\n body: unknown,\n url: string,\n headers?: Record<string, string>,\n kind: HttpErrorKind = \"response\",\n cause?: unknown,\n timeoutMs?: number\n ) {\n super(HttpErrorMessage.format(kind, url, status, rawMessage, timeoutMs));\n this.name = \"HttpError\";\n this.kind = kind;\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\n this.cause = cause;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * An error for a request that received no response: DNS failure, offline, CORS block, or a\n * rejected interceptor. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).\n */\n static network(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"network\", cause);\n }\n\n /**\n * An error for a request whose `timeout` option fired before the response, or its body, completed.\n * `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.\n *\n * @param {string} url The request URL.\n * @param {number} after The timeout in milliseconds.\n */\n static timeout(url: string, after: number): HttpError {\n return new HttpError(0, `Request timed out after ${after} ms`, null, url, undefined, \"timeout\", undefined, after);\n }\n\n /**\n * An error for a request that could not be built, so nothing was sent — typically a body that\n * could not be serialized. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).\n */\n static request(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"request\", cause);\n }\n\n /**\n * An error for a request aborted by something other than IO cancellation, such as an\n * interceptor aborting the signal. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} [cause] The abort reason, if any.\n */\n static aborted(url: string, cause?: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"aborted\", cause);\n }\n\n /**\n * Narrows the `unknown` error body to a concrete type using a runtime type guard.\n * Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,\n * not asserted. This keeps the unsafe cast out of consumer code: the body stays `unknown`\n * by default (forcing a conscious narrow), and this is the safe way to perform it.\n *\n * @template T The expected body shape.\n * @param {(x: unknown) => x is T} guard A type guard validating the body's shape.\n * @returns {Option<T>} `Some(body)` if the guard matches, `None` otherwise.\n *\n * @example\n * interface ApiError { code: string; message: string }\n * const isApiError = (x: unknown): x is ApiError =>\n * typeof x === \"object\" && x !== null && \"code\" in x && \"message\" in x;\n *\n * httpError.bodyAs(isApiError).fold(\n * () => \"unrecognized error shape\",\n * (e) => `${e.code}: ${e.message}`\n * );\n */\n bodyAs<T>(guard: (x: unknown) => x is T): Option<T> {\n return guard(this.body) ? (Some.of(this.body as NonNullable<T>) as Option<T>) : Option.empty<T>();\n }\n}\n","import { Credentials, Method, Options } from \"./options\";\n\n/** @internal */\nexport interface RequestDefaults {\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n}\n\n/** @internal */\nexport interface PreparedRequest {\n readonly method: Method;\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n readonly body: BodyInit | undefined;\n}\n\n/** Bodies `fetch` sends as-is: never JSON-encoded, never given an automatic content type. */\nconst isNativeBody = (body: unknown): boolean =>\n typeof body !== \"object\" ||\n body === null ||\n body instanceof FormData ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof URLSearchParams ||\n (typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream) ||\n ArrayBuffer.isView(body);\n\nconst normalizeHeaders = (headers: Record<string, string | undefined>): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([key, value]) => [key.toLowerCase(), value])\n );\n\n/** `FormData` must carry no content type so the browser can set the multipart boundary. */\nconst withContentType = (headers: Record<string, string>, body: unknown): Record<string, string> => {\n if (body instanceof FormData) {\n const { \"content-type\": _dropped, ...rest } = headers;\n return rest;\n }\n const autoJson = body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in headers);\n return autoJson ? { ...headers, \"content-type\": \"application/json\" } : headers;\n};\n\nconst serializeBody = (method: Method, body: unknown, headers: Record<string, string>): BodyInit | undefined =>\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? (body as BodyInit)\n : headers[\"content-type\"] === \"application/json\"\n ? JSON.stringify(body)\n : (body as BodyInit);\n\n/** @internal */\nexport const HttpRequest = {\n isNativeBody,\n normalizeHeaders,\n\n /** Per-request headers win over defaults; names are lowercased; the body follows the content type. */\n prepare: (method: Method, options: Options<unknown>, defaults: RequestDefaults): PreparedRequest => {\n const headers = withContentType(\n normalizeHeaders({ ...defaults.headers, ...(options.headers ?? {}) }),\n options.body\n );\n return {\n method,\n headers,\n credentials: options.credentials ?? defaults.credentials,\n body: serializeBody(method, options.body, headers),\n };\n },\n};\n","import { IO, Option } from \"@openstage/monadyssey-core\";\nimport { HttpError, HttpErrorMessage } from \"./http-error\";\nimport { ResponseType } from \"./options\";\n\ntype BodyReader = (response: Response) => Promise<unknown>;\n\nconst bodyReaders = {\n json: (response) => response.json(),\n text: (response) => response.text(),\n blob: (response) => response.blob(),\n arrayBuffer: (response) => response.arrayBuffer(),\n formData: (response) => response.formData(),\n} satisfies Record<ResponseType, BodyReader>;\n\n/** An own-property lookup, so an untyped `responseType` such as `\"toString\"` is rejected. */\nconst readerFor = (responseType: ResponseType): Option<BodyReader> =>\n Object.prototype.hasOwnProperty.call(bodyReaders, responseType)\n ? Option.of(bodyReaders[responseType])\n : Option.empty<BodyReader>();\n\nconst headers = (response: Response): Record<string, string> => {\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** The server answered but the body could not be used; the real status is kept, the body is `null`. */\nconst error = (cause: unknown, url: string, response: Response): HttpError =>\n new HttpError(response.status, HttpErrorMessage.describe(cause), null, url, headers(response), \"response\", cause);\n\n/** The server answered with a non-2xx status; the parsed body travels with the error. */\nconst failure = (response: Response, body: unknown, url: string): HttpError =>\n new HttpError(response.status, response.statusText, body, url, headers(response));\n\n/** @internal */\nexport const HttpResponse = {\n headers,\n error,\n failure,\n\n /** `204` and `205` carry no body and yield `null`. */\n read: (response: Response, responseType: ResponseType, url: string): IO<HttpError, unknown> =>\n IO.from(readerFor(responseType), () => error(`Unsupported response type: ${responseType}`, url, response)).flatMap(\n (read) =>\n IO.lift(\n () => (response.status === 204 || response.status === 205 ? null : read(response)),\n (cause) => error(cause, url, response)\n )\n ),\n\n settle: (response: Response, parsed: unknown, url: string): IO<HttpError, unknown> =>\n response.ok ? IO.of(parsed) : IO.fail(failure(response, parsed, url)),\n};\n","import { HttpError } from \"./http-error\";\n\nconst isAbort = (e: unknown): boolean =>\n typeof e === \"object\" && e !== null && \"name\" in e && (e as { name: unknown }).name === \"AbortError\";\n\n/** @internal */\nexport const HttpTransport = {\n /** An interceptor may reject with a ready-made `HttpError`; it is passed through untouched. */\n error: (e: unknown, url: string): HttpError =>\n e instanceof HttpError ? e : isAbort(e) ? HttpError.aborted(url, e) : HttpError.network(url, e),\n\n /** A timeout means the caller stopped waiting and an abort means someone wanted it stopped: neither is retried. */\n isRetryable: (error: HttpError): boolean =>\n error.kind === \"network\" || (error.kind === \"response\" && error.status >= 500),\n};\n","import { HttpInterceptor } from \"./options\";\n\n/** @internal */\nexport type Send = (request: RequestInit) => Promise<Response>;\n\n/** @internal */\nexport const Interceptors = {\n /** A right fold, so the first registered interceptor is outermost. */\n run: (interceptors: readonly HttpInterceptor[], request: RequestInit, send: Send): Promise<Response> =>\n interceptors.reduceRight<Send>((next, interceptor) => (r) => interceptor.intercept(r, next), send)(request),\n};\n","import { List, Stream } from \"@openstage/monadyssey-core\";\nimport type { Pipe } from \"@openstage/monadyssey-core\";\n\n/**\n * A single server-sent event parsed from a text/event-stream response.\n *\n * @property id - The event id, if the server sent an `id:` field. Used to resume a stream.\n * @property event - The event type. Defaults to `\"message\"` when the server sends no `event:` field.\n * @property data - The event payload. Multiple `data:` lines are joined with a newline.\n * @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.\n */\nexport interface ServerSentEvent {\n readonly id?: string;\n readonly event: string;\n readonly data: string;\n readonly retry?: number;\n}\n\n/** `null` when the block dispatches no event (comment-only, or no `data:`), per the event-stream rules. */\nconst parseBlock = (block: string): ServerSentEvent | null => {\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n const data: string[] = [];\n\n for (const line of block.split(\"\\n\")) {\n if (line === \"\" || line.startsWith(\":\")) continue; // blank line or comment\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n\n switch (field) {\n case \"event\":\n event = value;\n break;\n case \"data\":\n data.push(value);\n break;\n case \"id\":\n id = value;\n break;\n case \"retry\":\n if (/^\\d+$/.test(value)) retry = Number.parseInt(value, 10);\n break;\n default:\n break;\n }\n }\n\n if (data.length === 0) return null;\n return {\n event: event ?? \"message\",\n data: data.join(\"\\n\"),\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n };\n};\n\n/**\n * Reassembles a byte stream into complete events; partial input and split multi-byte characters\n * are buffered. Call `reset` when a new connection begins.\n *\n * @internal\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n push(chunk: Uint8Array): ServerSentEvent[] {\n let text = this.buffered + this.decoder.decode(chunk, { stream: true });\n let pendingCR = false;\n if (text.endsWith(\"\\r\")) {\n text = text.slice(0, -1); // hold a trailing CR: it may be the first half of a split CRLF\n pendingCR = true;\n }\n text = text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const blocks = text.split(\"\\n\\n\");\n let rest = blocks.pop() ?? \"\";\n if (pendingCR) rest += \"\\r\";\n this.buffered = rest;\n return blocks.map(parseBlock).filter((event): event is ServerSentEvent => event !== null);\n }\n\n reset(): void {\n this.decoder = new TextDecoder(\"utf-8\");\n this.buffered = \"\";\n }\n}\n\n/**\n * A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering\n * partial input across chunks. Single-use: each application maintains its own decoding state.\n *\n * @typeParam E - The error type carried by the surrounding stream.\n * @returns A pipe from a byte stream to a stream of parsed events.\n */\nexport const decodeEvents = <E>(): Pipe<E, Uint8Array, ServerSentEvent> => {\n const frames = new FrameBuffer();\n return (bytes: Stream<E, Uint8Array>) =>\n bytes.mapAccumulate(null, (chunk) => [null, List.from(frames.push(chunk))] as const);\n};\n","import { IO, Option, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { Credentials, HttpInterceptor } from \"./options\";\nimport { FrameBuffer, ServerSentEvent } from \"./sse\";\n\n/** A server-sent `retry:` below this is raised to it, so a misbehaving server cannot force a tight loop. */\nconst MIN_SERVER_RETRY_MS = 100;\n\ntype Reader = ReadableStreamDefaultReader<Uint8Array>;\n\n/** A cancellable sleep. The abort listener is removed when the timer fires, so waits do not accumulate listeners. */\nconst delay = (ms: number): IO<never, void> =>\n IO.cancellable<never, void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/**\n * Carried across reconnections within one run. Owns the one `AbortController` every connection\n * of the run is opened with, linked once to the run's signal; a connection registers a single\n * listener on it and removes it when closed, so listeners stay bounded however long the run lasts.\n */\nclass SseSession {\n readonly controller = new AbortController();\n readonly frames = new FrameBuffer();\n readonly pending: ServerSentEvent[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n private connection: { readonly reader: Reader; readonly unlink: () => void } | null = null;\n private readonly unlinkParent: () => void;\n\n constructor(retryMs: number, parent: AbortSignal) {\n this.retryMs = retryMs;\n const onAbort = () => this.controller.abort(parent.reason);\n parent.addEventListener(\"abort\", onAbort, { once: true });\n this.unlinkParent = () => parent.removeEventListener(\"abort\", onAbort);\n }\n\n get reader(): Reader | null {\n return this.connection?.reader ?? null;\n }\n\n attach(reader: Reader): void {\n const onAbort = () => reader.cancel().catch(() => {});\n this.controller.signal.addEventListener(\"abort\", onAbort, { once: true });\n this.connection = { reader, unlink: () => this.controller.signal.removeEventListener(\"abort\", onAbort) };\n this.frames.reset();\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.unlink();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n this.unlinkParent();\n this.controller.abort();\n }\n}\n\n/** @internal */\nexport interface SseConfig {\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly defaultHeaders: Record<string, string>;\n readonly credentials: Credentials;\n readonly interceptors: readonly HttpInterceptor[];\n readonly reconnect: boolean;\n readonly retry: number;\n}\n\nconst stream = (config: SseConfig): Stream<HttpError, ServerSentEvent> => {\n const { url, headers, defaultHeaders, credentials, interceptors, reconnect, retry } = config;\n\n const open = (session: SseSession): IO<HttpError, Reader> => {\n const prepared = HttpRequest.prepare(\n \"GET\",\n {\n headers: {\n ...headers,\n accept: \"text/event-stream\",\n ...(session.lastEventId !== undefined ? { \"last-event-id\": session.lastEventId } : {}),\n },\n credentials,\n },\n { headers: defaultHeaders, credentials }\n );\n return IO.lift<HttpError, Response>(\n () =>\n Interceptors.run(interceptors, { ...prepared, signal: session.controller.signal }, (req) => fetch(url, req)),\n (e: unknown) => HttpTransport.error(e, url)\n )\n .flatMap((response) =>\n response.ok\n ? IO.of(response)\n : HttpResponse.read(response, \"text\", url)\n .flatMapErr(() => IO.of<unknown>(null))\n .flatMap((body) => IO.fail<HttpError, Response>(HttpResponse.failure(response, body, url)))\n )\n .flatMap((response) =>\n response.body === null\n ? IO.fail<HttpError, Reader>(HttpResponse.error(\"Response has no body\", url, response))\n : IO.of(response.body.getReader())\n );\n };\n\n const readChunk = (reader: Reader): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.lift(\n () => reader.read(),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\n const done = IO.of<EventStep>(Option.empty());\n\n const retryLater = (session: SseSession): IO<HttpError, EventStep> =>\n delay(session.retryMs).flatMap(() => step(session));\n\n const step = (session: SseSession): IO<HttpError, EventStep> => {\n if (session.pending.length > 0) {\n const event = session.pending.shift() as ServerSentEvent;\n if (event.id !== undefined) session.lastEventId = event.id;\n if (event.retry !== undefined) session.retryMs = Math.max(event.retry, MIN_SERVER_RETRY_MS);\n return IO.of(Option.of<[ServerSentEvent, SseSession]>([event, session]));\n }\n\n const reader = session.reader;\n if (reader === null) {\n return open(session).foldM<HttpError, EventStep>(\n (error) => (reconnect && HttpTransport.isRetryable(error) ? retryLater(session) : IO.fail(error)),\n (opened) => {\n session.attach(opened);\n return step(session);\n }\n );\n }\n\n return readChunk(reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n return reconnect && HttpTransport.isRetryable(error) ? retryLater(session) : IO.fail(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n return reconnect ? retryLater(session) : done;\n }\n session.pending.push(...session.frames.push(result.value));\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.cancellable((signal: AbortSignal) => new SseSession(retry, signal)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.lift(() => session.close())\n );\n};\n\n/** @internal */\nexport const SseClient = { stream };\n","import { IO, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest, PreparedRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { SseClient } from \"./sse-client\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, SseOptions } from \"./options\";\nimport { ServerSentEvent } from \"./sse\";\n\n/**\n * A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of Promises.\n *\n * Unlike v1, `HttpClient` is instantiable — each instance carries its own configuration (base URL,\n * interceptors, default headers, timeout, credentials). This allows different parts of an application\n * to use independently configured clients.\n *\n * All HTTP methods return `IO<HttpError, A | null>`, enabling lazy execution, functional composition,\n * and explicit error handling. Cancellation is supported: when an IO is cancelled (via fiber or\n * timeout), the underlying `fetch` call is aborted through `AbortSignal`.\n *\n * **On the `| null` in the return type:** when the server responds with `204 No Content` or\n * `205 Reset Content`, there is no body to parse and the IO succeeds with `null`. This is a\n * protocol-level fact (the HTTP spec says these statuses have no body), distinct from a\n * domain-level optionality. For that reason the methods return `IO<HttpError, A | null>` rather\n * than `IO<HttpError, Option<A>>`. `Option` is the right tool when a value might be absent at\n * the domain level; `| null` is honest when the absence is structural to the protocol. For the\n * 95% case where you control the endpoint and never receive 204, the `| null` is a brief\n * `?? defaultValue` away.\n *\n * **Default credentials:** `same-origin`, matching the platform `fetch` default. Set\n * `credentials: \"include\"` explicitly per-client or per-request if you need cookies to flow\n * cross-origin.\n *\n * @example\n * const api = new HttpClient({\n * baseUrl: \"https://api.example.com\",\n * interceptors: [authInterceptor],\n * defaultHeaders: { \"Accept\": \"application/json\" },\n * timeout: 5000,\n * });\n *\n * const users = api.get<User[]>(\"/users\");\n */\nexport class HttpClient {\n private readonly interceptors: readonly HttpInterceptor[];\n private readonly baseUrl: string | undefined;\n private readonly defaultHeaders: Record<string, string>;\n private readonly defaultTimeout: number | undefined;\n private readonly defaultCredentials: Credentials;\n\n /**\n * Creates a new HttpClient with the given configuration.\n *\n * @param {HttpClientConfig} config - Configuration for the client.\n */\n constructor(config: HttpClientConfig = {}) {\n this.interceptors = Object.freeze([...(config.interceptors ?? [])]);\n this.baseUrl = config.baseUrl;\n this.defaultHeaders = config.defaultHeaders ?? {};\n this.defaultTimeout = config.timeout;\n this.defaultCredentials = config.credentials ?? \"same-origin\";\n }\n\n /**\n * Performs a GET request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Omit<Options<A>, \"body\">} [options] - Request options (excluding body).\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n get(uri: string, options: Omit<Options, \"body\"> & { observe: \"response\" }): IO<HttpError, Response>;\n get<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, A | null>;\n get<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"GET\", options);\n }\n\n /**\n * Performs a POST request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n post(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n post<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n post<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"POST\", { ...options, body });\n }\n\n /**\n * Performs a PUT request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n put(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n put<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n put<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"PUT\", { ...options, body });\n }\n\n /**\n * Performs a PATCH request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n patch(uri: string, body: unknown, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n patch<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, A | null>;\n patch<A = unknown>(uri: string, body?: unknown, options?: Options<A>): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"PATCH\", { ...options, body });\n }\n\n /**\n * Performs a DELETE request.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Omit<Options<A>, \"body\">} [options] - Request options (excluding body).\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n delete(uri: string, options: Omit<Options, \"body\"> & { observe: \"response\" }): IO<HttpError, Response>;\n delete<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, A | null>;\n delete<A = unknown>(uri: string, options?: Omit<Options<A>, \"body\">): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, \"DELETE\", options);\n }\n\n /**\n * Performs a custom HTTP request with the specified method.\n *\n * @template A - The expected type of the response body.\n * @param {string} uri - The URL or path to request.\n * @param {Method} method - The HTTP method.\n * @param {Options<A>} [options] - Request options.\n * @returns {IO<HttpError, A | null>} An IO representing the result.\n */\n fetch(uri: string, method: Method, options: Options & { observe: \"response\" }): IO<HttpError, Response>;\n fetch<A = unknown>(uri: string, method: Method, options?: Options<A>): IO<HttpError, A | null>;\n fetch<A = unknown>(uri: string, method: Method, options: Options<A> = {}): IO<HttpError, Response | A | null> {\n return this.request<A>(uri, method, options);\n }\n\n /**\n * Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.\n *\n * The request goes through the same pipeline as every other method, so headers (including\n * `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is\n * used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts\n * the connection and releases resources.\n *\n * By default the stream reconnects automatically when the connection ends or fails with a\n * retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers\n * and credentials on each attempt; terminal `4xx` responses (including auth failures) end the\n * stream. See {@link SseOptions} to configure or disable this.\n *\n * @param {string} uri - The URL or path to connect to.\n * @param {SseOptions} [options] - Headers, credentials, and reconnection settings.\n * @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.\n *\n * @example\n * const events = client.sse(\"/events\", { headers: { Authorization: `Bearer ${token}` } });\n * await events.runForEach((event) => IO.lift(() => handle(event))).unsafeRun();\n */\n sse(uri: string, options: SseOptions = {}): Stream<HttpError, ServerSentEvent> {\n return SseClient.stream({\n url: this.resolveUrl(uri),\n headers: options.headers ?? {},\n defaultHeaders: this.defaultHeaders,\n credentials: options.credentials ?? this.defaultCredentials,\n interceptors: this.interceptors,\n reconnect: options.reconnect ?? true,\n retry: options.retry ?? 3000,\n });\n }\n\n private resolveUrl(uri: string): string {\n if (!this.baseUrl) return uri;\n try {\n new URL(uri);\n return uri;\n } catch {\n const base = this.baseUrl.endsWith(\"/\") ? this.baseUrl.slice(0, -1) : this.baseUrl;\n const path = uri.startsWith(\"/\") ? uri : `/${uri}`;\n return `${base}${path}`;\n }\n }\n\n private request<A = unknown>(\n uri: string,\n method: Method,\n options: Options<A> = {}\n ): IO<HttpError, Response | A | null> {\n const url = this.resolveUrl(uri);\n const timeout = options.timeout ?? this.defaultTimeout;\n const { responseType = \"json\", observe = \"body\", transform = (data: unknown) => data as A } = options;\n const defaults = { headers: this.defaultHeaders, credentials: this.defaultCredentials };\n\n const prepare: IO<HttpError, PreparedRequest> = IO.lift(\n () => HttpRequest.prepare(method, options, defaults),\n (e: unknown) => HttpError.request(url, e)\n );\n\n const send = (prepared: PreparedRequest): IO<HttpError, Response> =>\n IO.cancellable(\n (signal: AbortSignal) => Interceptors.run(this.interceptors, { ...prepared, signal }, (req) => fetch(url, req)),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n const decode = (response: Response): IO<HttpError, A | null> =>\n HttpResponse.read(response, responseType, url)\n .flatMap((parsed) => HttpResponse.settle(response, parsed, url))\n .flatMap((parsed) =>\n IO.lift(\n () => transform(parsed),\n (e: unknown) => HttpResponse.error(e, url, response)\n )\n );\n\n const outcome = prepare\n .flatMap(send)\n .flatMap((response): IO<HttpError, Response | A | null> =>\n response.ok && observe === \"response\" ? IO.of(response) : decode(response)\n );\n\n return timeout == null ? outcome : outcome.timeout(timeout, () => HttpError.timeout(url, timeout));\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@openstage/monadyssey-core")):typeof define==`function`&&define.amd?define([`exports`,`@openstage/monadyssey-core`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e[`monadyssey-fetch`]={},e.Monadyssey))})(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n={describe:e=>e instanceof Error?e.message:typeof e==`string`?e:typeof e==`object`&&e&&`message`in e?String(e.message):`An unknown error occurred.`,format:(e,t,n,r,i)=>{switch(e){case`response`:return`Request to '${t}' failed with status ${n} and message: ${r}.`;case`request`:return`Request to '${t}' could not be sent: ${r}.`;case`timeout`:return`Request to '${t}' timed out after ${i} ms.`;case`aborted`:return`Request to '${t}' was aborted before a response arrived: ${r}.`;default:return`Request to '${t}' failed before a response arrived: ${r}.`}}},r=class e extends Error{kind;status;rawMessage;body;url;headers;cause;timeoutMs;constructor(e,t,r,i,a,o=`response`,s,c){super(n.format(o,i,e,t,c)),this.name=`HttpError`,this.kind=o,this.status=e,this.rawMessage=t,this.body=r,this.url=i,this.headers=a,this.cause=s,this.timeoutMs=c}static network(t,r){return new e(0,n.describe(r),null,t,void 0,`network`,r)}static timeout(t,n){return new e(0,`Request timed out after ${n} ms`,null,t,void 0,`timeout`,void 0,n)}static request(t,r){return new e(0,n.describe(r),null,t,void 0,`request`,r)}static aborted(t,r){return new e(0,n.describe(r),null,t,void 0,`aborted`,r)}bodyAs(e){return e(this.body)?t.Some.of(this.body):t.Option.empty()}},i=e=>typeof e!=`object`||!e||e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||e instanceof URLSearchParams||typeof ReadableStream<`u`&&e instanceof ReadableStream||ArrayBuffer.isView(e),a=e=>Object.fromEntries(Object.entries(e).filter(e=>e[1]!==void 0).map(([e,t])=>[e.toLowerCase(),t])),o=(e,t)=>{if(t instanceof FormData){let{"content-type":t,...n}=e;return n}return typeof t==`object`&&t&&!i(t)&&!(`content-type`in e)?{...e,"content-type":`application/json`}:e},s=(e,t,n)=>e===`GET`||e===`HEAD`||t==null?void 0:i(t)?t:n[`content-type`]===`application/json`?JSON.stringify(t):t,c={isNativeBody:i,normalizeHeaders:a,prepare:(e,t,n)=>{let r=o(a({...n.headers,...t.headers??{}}),t.body);return{method:e,headers:r,credentials:t.credentials??n.credentials,body:s(e,t.body,r)}}},l={json:e=>e.json(),text:e=>e.text(),blob:e=>e.blob(),arrayBuffer:e=>e.arrayBuffer(),formData:e=>e.formData()},u=e=>Object.prototype.hasOwnProperty.call(l,e)?t.Option.of(l[e]):t.Option.empty(),d=e=>{try{return Object.fromEntries(e.headers.entries())}catch{return{}}},f=(e,t,i)=>new r(i.status,n.describe(e),null,t,d(i),`response`,e),p=(e,t,n)=>new r(e.status,e.statusText,t,n,d(e)),m={headers:d,error:f,failure:p,read:(e,n,r)=>t.IO.from(u(n),()=>f(`Unsupported response type: ${n}`,r,e)).flatMap(n=>t.IO.lift(()=>e.status===204||e.status===205?null:n(e),t=>f(t,r,e))),settle:(e,n,r)=>e.ok?t.IO.of(n):t.IO.fail(p(e,n,r))},h=e=>typeof e==`object`&&!!e&&`name`in e&&e.name===`AbortError`,g={error:(e,t)=>e instanceof r?e:h(e)?r.aborted(t,e):r.network(t,e),isRetryable:e=>e.kind===`network`||e.kind===`response`&&e.status>=500},_={run:(e,t,n)=>e.reduceRight((e,t)=>n=>t.intercept(n,e),n)(t)},v=e=>{let t,n,r,i=[];for(let a of e.split(`
|
|
2
|
-
`)){if(a===``||a.startsWith(`:`))continue;let e=a.indexOf(`:`),o=e===-1?a:a.slice(0,e),s=e===-1?``:a.slice(e+1);switch(s.startsWith(` `)&&(s=s.slice(1)),o){case`event`:t=s;break;case`data`:i.push(s);break;case`id`:n=s;break;case`retry`:/^\d+$/.test(s)&&(r=Number.parseInt(s,10))}}return i.length===0?null:{event:t??`message`,data:i.join(`
|
|
3
|
-
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},y=class{decoder=new TextDecoder(`utf-8`);buffered=``;push(e){let t=this.buffered+this.decoder.decode(e,{stream:!0}),n=!1;t.endsWith(`\r`)&&(t=t.slice(0,-1),n=!0),t=t.replace(/\r\n/g,`
|
|
4
|
-
`).replace(/\r/g,`
|
|
5
|
-
`);let r=t.split(`
|
|
6
|
-
|
|
7
|
-
`),i=r.pop()??``;return n&&(i+=`\r`),this.buffered=i,r.map(v).filter(e=>e!==null)}reset(){this.decoder=new TextDecoder(`utf-8`),this.buffered=``}},b=()=>{let e=new y;return n=>n.mapAccumulate(null,n=>[null,t.List.from(e.push(n))])},x=100,S=e=>t.IO.cancellable(t=>new Promise(n=>{let r=()=>{clearTimeout(i),n()},i=setTimeout(()=>{t.removeEventListener(`abort`,r),n()},e);t.addEventListener(`abort`,r,{once:!0})})),C=class{controller=new AbortController;frames=new y;pending=[];lastEventId=void 0;retryMs;connection=null;unlinkParent;constructor(e,t){this.retryMs=e;let n=()=>this.controller.abort(t.reason);t.addEventListener(`abort`,n,{once:!0}),this.unlinkParent=()=>t.removeEventListener(`abort`,n)}get reader(){return this.connection?.reader??null}attach(e){let t=()=>e.cancel().catch(()=>{});this.controller.signal.addEventListener(`abort`,t,{once:!0}),this.connection={reader:e,unlink:()=>this.controller.signal.removeEventListener(`abort`,t)},this.frames.reset()}closeConnection(){this.connection&&=(this.connection.unlink(),this.connection.reader.cancel().catch(()=>{}),null)}close(){this.closeConnection(),this.unlinkParent(),this.controller.abort()}},w={stream:e=>{let{url:n,headers:r,defaultHeaders:i,credentials:a,interceptors:o,reconnect:s,retry:l}=e,u=e=>{let s=c.prepare(`GET`,{headers:{...r,accept:`text/event-stream`,...e.lastEventId===void 0?{}:{"last-event-id":e.lastEventId}},credentials:a},{headers:i,credentials:a});return t.IO.lift(()=>_.run(o,{...s,signal:e.controller.signal},e=>fetch(n,e)),e=>g.error(e,n)).flatMap(e=>e.ok?t.IO.of(e):m.read(e,`text`,n).flatMapErr(()=>t.IO.of(null)).flatMap(r=>t.IO.fail(m.failure(e,r,n)))).flatMap(e=>e.body===null?t.IO.fail(m.error(`Response has no body`,n,e)):t.IO.of(e.body.getReader()))},d=e=>t.IO.lift(()=>e.read(),e=>g.error(e,n)),f=t.IO.of(t.Option.empty()),p=e=>S(e.retryMs).flatMap(()=>h(e)),h=e=>{if(e.pending.length>0){let n=e.pending.shift();return n.id!==void 0&&(e.lastEventId=n.id),n.retry!==void 0&&(e.retryMs=Math.max(n.retry,x)),t.IO.of(t.Option.of([n,e]))}let n=e.reader;return n===null?u(e).foldM(n=>s&&g.isRetryable(n)?p(e):t.IO.fail(n),t=>(e.attach(t),h(e))):d(n).foldM(n=>(e.closeConnection(),s&&g.isRetryable(n)?p(e):t.IO.fail(n)),t=>t.done?(e.closeConnection(),s?p(e):f):(e.pending.push(...e.frames.push(t.value)),h(e)))};return t.Stream.bracket(t.IO.cancellable(e=>new C(l,e)),e=>t.Stream.unfoldEval(e,h),e=>t.IO.lift(()=>e.close()))}};e.HttpClient=class{interceptors;baseUrl;defaultHeaders;defaultTimeout;defaultCredentials;constructor(e={}){this.interceptors=Object.freeze([...e.interceptors??[]]),this.baseUrl=e.baseUrl,this.defaultHeaders=e.defaultHeaders??{},this.defaultTimeout=e.timeout,this.defaultCredentials=e.credentials??`same-origin`}get(e,t){return this.request(e,`GET`,t)}post(e,t,n){return this.request(e,`POST`,{...n,body:t})}put(e,t,n){return this.request(e,`PUT`,{...n,body:t})}patch(e,t,n){return this.request(e,`PATCH`,{...n,body:t})}delete(e,t){return this.request(e,`DELETE`,t)}fetch(e,t,n={}){return this.request(e,t,n)}sse(e,t={}){return w.stream({url:this.resolveUrl(e),headers:t.headers??{},defaultHeaders:this.defaultHeaders,credentials:t.credentials??this.defaultCredentials,interceptors:this.interceptors,reconnect:t.reconnect??!0,retry:t.retry??3e3})}resolveUrl(e){if(!this.baseUrl)return e;try{return new URL(e),e}catch{return`${this.baseUrl.endsWith(`/`)?this.baseUrl.slice(0,-1):this.baseUrl}${e.startsWith(`/`)?e:`/${e}`}`}}request(e,n,i={}){let a=this.resolveUrl(e),o=i.timeout??this.defaultTimeout,{responseType:s=`json`,observe:l=`body`,transform:u=e=>e}=i,d={headers:this.defaultHeaders,credentials:this.defaultCredentials},f=t.IO.lift(()=>c.prepare(n,i,d),e=>r.request(a,e)),p=e=>t.IO.cancellable(t=>_.run(this.interceptors,{...e,signal:t},e=>fetch(a,e)),e=>g.error(e,a)),h=e=>m.read(e,s,a).flatMap(t=>m.settle(e,t,a)).flatMap(n=>t.IO.lift(()=>u(n),t=>m.error(t,a,e))),v=f.flatMap(p).flatMap(e=>e.ok&&l===`response`?t.IO.of(e):h(e));return o==null?v:v.timeout(o,()=>r.timeout(a,o))}},e.HttpError=r,e.decodeEvents=b});
|
|
8
|
-
//# sourceMappingURL=monadyssey-fetch.umd.js.map
|