@openstage/monadyssey-fetch 3.0.0-beta.5 → 3.0.0-beta.7
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/dist/monadyssey-fetch.cjs +5 -5
- package/dist/monadyssey-fetch.cjs.map +1 -1
- package/dist/monadyssey-fetch.d.ts +71 -4
- package/dist/monadyssey-fetch.mjs +163 -136
- package/dist/monadyssey-fetch.mjs.map +1 -1
- package/dist/monadyssey-fetch.umd.js +5 -5
- package/dist/monadyssey-fetch.umd.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +5 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@openstage/monadyssey-core");var 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
|
|
3
|
-
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},
|
|
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
4
|
`).replace(/\r/g,`
|
|
5
|
-
`);let
|
|
5
|
+
`);let r=t.split(`
|
|
6
6
|
|
|
7
|
-
`),
|
|
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
8
|
//# sourceMappingURL=monadyssey-fetch.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.cjs","names":[],"sources":["../src/sse.ts","../src/http-client.ts"],"sourcesContent":["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/**\n * Parses a single event block (the text between two blank lines) into a {@link ServerSentEvent}.\n * Returns `null` when the block dispatches no event (e.g. a comment-only or field-only block with\n * no `data:`), matching the event-stream dispatch rules.\n */\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 const parsed = Number.parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\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 * Incrementally reassembles a byte stream into complete {@link ServerSentEvent}s. Feed it chunks\n * with {@link push}; it buffers partial input, so events split across byte boundaries — and\n * multi-byte characters split mid-character — are reassembled correctly. Comment/heartbeat blocks\n * and an unterminated trailing block yield nothing.\n *\n * Stateful and single-connection: call {@link reset} (or use a fresh instance) when a new\n * connection begins, so an incomplete event from a dropped connection is discarded.\n *\n * Package-internal — not part of the public API.\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n /** Decodes and frames a chunk, returning any events completed by it. */\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 /** Discards buffered partial input, e.g. before reconnecting. */\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, Some, Stream } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType, SseOptions } from \"./options\";\nimport { FrameBuffer, ServerSentEvent } from \"./sse\";\n\n/**\n * Returns `true` if the body is a type that the fetch API knows how to send natively.\n * These types must NOT be JSON.stringified and must NOT have a Content-Type header auto-set\n * (the browser handles multipart boundaries for FormData, etc.).\n */\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\n/**\n * Normalizes header keys to lowercase for case-insensitive comparison.\n * HTTP header names are case-insensitive per RFC 7230.\n */\nconst normalizeHeaders = (headers: Record<string, string>): Record<string, string> => {\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n result[key.toLowerCase()] = value;\n }\n }\n return result;\n};\n\n/**\n * Builds the interceptor chain as a pure function. No global state.\n * Interceptors are applied in registration order — first registered is outermost.\n */\nconst runInterceptors = (\n interceptors: readonly HttpInterceptor[],\n req: RequestInit,\n fn: (req: RequestInit) => Promise<Response>\n): Promise<Response> => {\n let next = fn;\n for (const interceptor of [...interceptors].reverse()) {\n const currentNext = next;\n next = (r: RequestInit) => interceptor.intercept(r, currentNext);\n }\n return next(req);\n};\n\nconst parseBody = async (response: Response, responseType: ResponseType, url: string): Promise<unknown> => {\n if (response.status === 204 || response.status === 205) {\n return null;\n }\n try {\n switch (responseType) {\n case \"json\":\n return await response.json();\n case \"text\":\n return await response.text();\n case \"blob\":\n return await response.blob();\n case \"arrayBuffer\":\n return await response.arrayBuffer();\n case \"formData\":\n return await response.formData();\n default:\n throw toHttpError(`Unsupported response type: ${responseType}`, url, response);\n }\n } catch (e: unknown) {\n if (e instanceof HttpError) throw e;\n throw toHttpError(e, url, response);\n }\n};\n\nconst toHttpError = (e: unknown, uri: string, response?: Response): HttpError => {\n const headers = extractHeadersFrom(response);\n const body = response ? response.body : null;\n\n const message =\n e instanceof Error\n ? e.message\n : typeof e === \"string\"\n ? e\n : typeof e === \"object\" && e !== null && \"message\" in e\n ? String((e as any).message)\n : \"An unknown error occurred.\";\n\n return new HttpError(response?.status || 500, message, body, uri, headers);\n};\n\nconst extractHeadersFrom = (response?: Response): Record<string, string> => {\n if (!response) return {};\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** A retryable failure is a network error or a server error; `4xx` responses are terminal. */\nconst isRetryable = (error: HttpError): boolean => error.status >= 500;\n\n/** A cancellable delay — an abort resolves it immediately so a pending reconnect wait is interruptible. */\nconst delay = <E = never>(ms: number): IO<E, void> =>\n IO.cancellable<E, void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/** A live SSE connection: the body reader and the controller that aborts it. */\ninterface Connection {\n readonly reader: ReadableStreamDefaultReader<Uint8Array>;\n readonly controller: AbortController;\n}\n\n/** Mutable state for a single `sse` run, carried across reconnections. */\nclass SseSession {\n connection: Connection | null = null;\n readonly frames = new FrameBuffer();\n readonly pending: ServerSentEvent[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n shouldDelay = false;\n\n constructor(retryMs: number) {\n this.retryMs = retryMs;\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.controller.abort();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n }\n}\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 const reconnect = options.reconnect ?? true;\n const defaultRetry = options.retry ?? 3000;\n const resolvedUrl = this.resolveUrl(uri);\n const requestHeaders = options.headers ?? {};\n const credentials = options.credentials ?? this.defaultCredentials;\n const interceptors = this.interceptors;\n const defaultHeaders = this.defaultHeaders;\n\n const open = (lastEventId: string | undefined): IO<HttpError, Connection> =>\n IO.lift<HttpError, Connection>(\n async (signal?: AbortSignal) => {\n const controller = new AbortController();\n if (signal) {\n if (signal.aborted) controller.abort(signal.reason);\n else signal.addEventListener(\"abort\", () => controller.abort(signal.reason), { once: true });\n }\n const headers = normalizeHeaders({\n ...defaultHeaders,\n ...requestHeaders,\n accept: \"text/event-stream\",\n ...(lastEventId !== undefined ? { \"last-event-id\": lastEventId } : {}),\n });\n const requestInit: RequestInit = { method: \"GET\", headers, credentials, signal: controller.signal };\n const response = await runInterceptors(interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n if (!response.ok) {\n const body = await parseBody(response, \"text\", resolvedUrl).catch(() => null);\n throw new HttpError(response.status, response.statusText, body, resolvedUrl, extractHeadersFrom(response));\n }\n if (!response.body) {\n throw new HttpError(500, \"Response has no body\", null, resolvedUrl, extractHeadersFrom(response));\n }\n return { reader: response.body.getReader(), controller };\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n const readChunk = (\n reader: ReadableStreamDefaultReader<Uint8Array>\n ): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.cancellable<HttpError, ReadableStreamReadResult<Uint8Array>>(\n (signal: AbortSignal) => {\n const onAbort = () => reader.cancel().catch(() => {});\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n return reader.read();\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\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 = event.retry;\n return IO.of(Option.of<[ServerSentEvent, SseSession]>([event, session]));\n }\n\n if (session.connection === null) {\n const connect = open(session.lastEventId).foldM<HttpError, EventStep>(\n (error) => {\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (connection) => {\n session.connection = connection;\n session.frames.reset();\n session.shouldDelay = false;\n return step(session);\n }\n );\n return session.shouldDelay ? delay<HttpError>(session.retryMs).flatMap(() => connect) : connect;\n }\n\n return readChunk(session.connection.reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n if (reconnect) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.of(Option.empty<[ServerSentEvent, SseSession]>());\n }\n if (result.value !== undefined) {\n session.pending.push(...session.frames.push(result.value));\n }\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.lift(() => new SseSession(defaultRetry)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.lift(() => session.close())\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 resolvedUrl = this.resolveUrl(uri);\n const timeout = options.timeout ?? this.defaultTimeout;\n\n return IO.cancellable<HttpError, Response | A | null>(\n async (signal: AbortSignal) => {\n const {\n headers = {},\n body,\n responseType = \"json\",\n credentials = this.defaultCredentials,\n observe = \"body\",\n transform = (data: unknown) => data as A,\n } = options;\n\n const mergedHeaders = normalizeHeaders({ ...this.defaultHeaders, ...headers });\n\n const shouldAutoJson =\n body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in mergedHeaders);\n\n if (shouldAutoJson) {\n mergedHeaders[\"content-type\"] = \"application/json\";\n }\n\n if (body instanceof FormData) {\n delete mergedHeaders[\"content-type\"];\n }\n\n const serializedBody =\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? body\n : mergedHeaders[\"content-type\"] === \"application/json\"\n ? JSON.stringify(body)\n : body;\n\n let controller: AbortController | undefined;\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const fetchSignal = (() => {\n if (timeout != null) {\n controller = new AbortController();\n const onParentAbort = () => controller!.abort(signal.reason);\n signal.addEventListener(\"abort\", onParentAbort, { once: true });\n timeoutId = setTimeout(\n () => controller!.abort(new DOMException(\"Request timed out\", \"TimeoutError\")),\n timeout\n );\n return controller.signal;\n }\n return signal;\n })();\n\n try {\n const requestInit: RequestInit = {\n method,\n headers: mergedHeaders,\n credentials,\n body: serializedBody as BodyInit | undefined,\n signal: fetchSignal,\n };\n\n const response = await runInterceptors(this.interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n\n if (!response.ok) {\n const rb = await parseBody(response, responseType, resolvedUrl);\n const respHeaders = extractHeadersFrom(response);\n throw new HttpError(response.status, response.statusText, rb, resolvedUrl, respHeaders);\n }\n\n if (observe === \"response\") {\n return response;\n }\n\n const rb = await parseBody(response, responseType, resolvedUrl);\n return transform(rb);\n } finally {\n if (timeoutId != null) clearTimeout(timeoutId);\n }\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n }\n}\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: HTTP status code,\n * raw error message, response body, request URL, and response headers.\n */\nexport class HttpError extends Error {\n public readonly status: number;\n public readonly rawMessage: string;\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n\n constructor(status: number, rawMessage: string, body: unknown, url: string, headers?: Record<string, string>) {\n super(`Request to '${url}' failed with status ${status} and message: ${rawMessage}.`);\n this.name = \"HttpError\";\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\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"],"mappings":"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"}
|
|
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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"}
|
|
@@ -187,16 +187,68 @@ export declare type HttpClientConfig = {
|
|
|
187
187
|
/**
|
|
188
188
|
* Represents an HTTP error encountered during a request.
|
|
189
189
|
*
|
|
190
|
-
* Extends the native `Error` class with additional context:
|
|
191
|
-
* raw error message, response body, request URL,
|
|
190
|
+
* Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP
|
|
191
|
+
* status code, raw error message, response body, request URL, response headers, and the
|
|
192
|
+
* underlying cause.
|
|
193
|
+
*
|
|
194
|
+
* `kind` tells the two facts apart that `status` alone cannot: "the server said 500" is
|
|
195
|
+
* `kind: "response", status: 500`, while "nothing answered" is `kind: "network", status: 0`.
|
|
196
|
+
* For every kind other than `"response"`, `status` is `0` and `body` is `null`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* client.post("/payments", payment).mapErr((error) =>
|
|
200
|
+
* error.kind === "response"
|
|
201
|
+
* ? { kind: "rejected", status: error.status }
|
|
202
|
+
* : { kind: "outcomeUnknown" } // the request may or may not have reached the server
|
|
203
|
+
* );
|
|
192
204
|
*/
|
|
193
205
|
export declare class HttpError extends Error {
|
|
206
|
+
/** The transport outcome. See {@link HttpErrorKind}. */
|
|
207
|
+
readonly kind: HttpErrorKind;
|
|
208
|
+
/** The HTTP status code, or `0` when no response arrived (`kind !== "response"`). */
|
|
194
209
|
readonly status: number;
|
|
195
210
|
readonly rawMessage: string;
|
|
211
|
+
/** The parsed response body, or `null` when none was parsed. */
|
|
196
212
|
readonly body: unknown;
|
|
197
213
|
readonly url: string;
|
|
198
214
|
readonly headers?: Record<string, string>;
|
|
199
|
-
|
|
215
|
+
/** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */
|
|
216
|
+
readonly cause?: unknown;
|
|
217
|
+
/** The timeout that fired, in milliseconds. Set only when `kind === "timeout"`. */
|
|
218
|
+
readonly timeoutMs?: number;
|
|
219
|
+
constructor(status: number, rawMessage: string, body: unknown, url: string, headers?: Record<string, string>, kind?: HttpErrorKind, cause?: unknown, timeoutMs?: number);
|
|
220
|
+
/**
|
|
221
|
+
* An error for a request that received no response: DNS failure, offline, CORS block, or a
|
|
222
|
+
* rejected interceptor. `status` is `0` and `body` is `null`.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} url The request URL.
|
|
225
|
+
* @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).
|
|
226
|
+
*/
|
|
227
|
+
static network(url: string, cause: unknown): HttpError;
|
|
228
|
+
/**
|
|
229
|
+
* An error for a request whose `timeout` option fired before the response, or its body, completed.
|
|
230
|
+
* `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} url The request URL.
|
|
233
|
+
* @param {number} after The timeout in milliseconds.
|
|
234
|
+
*/
|
|
235
|
+
static timeout(url: string, after: number): HttpError;
|
|
236
|
+
/**
|
|
237
|
+
* An error for a request that could not be built, so nothing was sent — typically a body that
|
|
238
|
+
* could not be serialized. `status` is `0` and `body` is `null`.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} url The request URL.
|
|
241
|
+
* @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).
|
|
242
|
+
*/
|
|
243
|
+
static request(url: string, cause: unknown): HttpError;
|
|
244
|
+
/**
|
|
245
|
+
* An error for a request aborted by something other than IO cancellation, such as an
|
|
246
|
+
* interceptor aborting the signal. `status` is `0` and `body` is `null`.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} url The request URL.
|
|
249
|
+
* @param {unknown} [cause] The abort reason, if any.
|
|
250
|
+
*/
|
|
251
|
+
static aborted(url: string, cause?: unknown): HttpError;
|
|
200
252
|
/**
|
|
201
253
|
* Narrows the `unknown` error body to a concrete type using a runtime type guard.
|
|
202
254
|
* Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,
|
|
@@ -220,6 +272,20 @@ export declare class HttpError extends Error {
|
|
|
220
272
|
bodyAs<T>(guard: (x: unknown) => x is T): Option_2<T>;
|
|
221
273
|
}
|
|
222
274
|
|
|
275
|
+
/**
|
|
276
|
+
* How a request failed.
|
|
277
|
+
* - `"request"`: the request could not be built — typically the body could not be serialized.
|
|
278
|
+
* Nothing was sent.
|
|
279
|
+
* - `"response"`: the server answered with a non-2xx status, or a 2xx whose body could not be parsed.
|
|
280
|
+
* - `"network"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.
|
|
281
|
+
* - `"timeout"`: the client's `timeout` option fired before the response, or its body, completed.
|
|
282
|
+
* The request may have reached the server.
|
|
283
|
+
* - `"aborted"`: the request was aborted by something other than IO cancellation, such as an
|
|
284
|
+
* interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as
|
|
285
|
+
* `Cancelled`, not as an `HttpError`).
|
|
286
|
+
*/
|
|
287
|
+
export declare type HttpErrorKind = "request" | "response" | "network" | "timeout" | "aborted";
|
|
288
|
+
|
|
223
289
|
/**
|
|
224
290
|
* An HTTP interceptor that can modify or handle request and response data
|
|
225
291
|
* before and after an outgoing `fetch` call.
|
|
@@ -315,7 +381,8 @@ export declare type SseOptions = {
|
|
|
315
381
|
reconnect?: boolean;
|
|
316
382
|
/**
|
|
317
383
|
* The delay in milliseconds before reconnecting, used until the server sends a `retry:` value.
|
|
318
|
-
*
|
|
384
|
+
* A server-sent value below 100 ms is raised to 100 ms so a misbehaving server cannot make the
|
|
385
|
+
* client reconnect in a tight loop. Defaults to `3000`.
|
|
319
386
|
*/
|
|
320
387
|
retry?: number;
|
|
321
388
|
};
|
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
import { IO as e, List as t, Option as n, Some as r, Stream as i } from "@openstage/monadyssey-core";
|
|
2
|
-
//#region src/
|
|
3
|
-
var a =
|
|
2
|
+
//#region src/http-error.ts
|
|
3
|
+
var a = {
|
|
4
|
+
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.",
|
|
5
|
+
format: (e, t, n, r, i) => {
|
|
6
|
+
switch (e) {
|
|
7
|
+
case "response": return `Request to '${t}' failed with status ${n} and message: ${r}.`;
|
|
8
|
+
case "request": return `Request to '${t}' could not be sent: ${r}.`;
|
|
9
|
+
case "timeout": return `Request to '${t}' timed out after ${i} ms.`;
|
|
10
|
+
case "aborted": return `Request to '${t}' was aborted before a response arrived: ${r}.`;
|
|
11
|
+
default: return `Request to '${t}' failed before a response arrived: ${r}.`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}, o = class e extends Error {
|
|
15
|
+
kind;
|
|
16
|
+
status;
|
|
17
|
+
rawMessage;
|
|
18
|
+
body;
|
|
19
|
+
url;
|
|
20
|
+
headers;
|
|
21
|
+
cause;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
constructor(e, t, n, r, i, o = "response", s, c) {
|
|
24
|
+
super(a.format(o, r, e, t, c)), this.name = "HttpError", this.kind = o, this.status = e, this.rawMessage = t, this.body = n, this.url = r, this.headers = i, this.cause = s, this.timeoutMs = c;
|
|
25
|
+
}
|
|
26
|
+
static network(t, n) {
|
|
27
|
+
return new e(0, a.describe(n), null, t, void 0, "network", n);
|
|
28
|
+
}
|
|
29
|
+
static timeout(t, n) {
|
|
30
|
+
return new e(0, `Request timed out after ${n} ms`, null, t, void 0, "timeout", void 0, n);
|
|
31
|
+
}
|
|
32
|
+
static request(t, n) {
|
|
33
|
+
return new e(0, a.describe(n), null, t, void 0, "request", n);
|
|
34
|
+
}
|
|
35
|
+
static aborted(t, n) {
|
|
36
|
+
return new e(0, a.describe(n), null, t, void 0, "aborted", n);
|
|
37
|
+
}
|
|
38
|
+
bodyAs(e) {
|
|
39
|
+
return e(this.body) ? r.of(this.body) : n.empty();
|
|
40
|
+
}
|
|
41
|
+
}, s = (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), c = (e) => Object.fromEntries(Object.entries(e).filter((e) => e[1] !== void 0).map(([e, t]) => [e.toLowerCase(), t])), l = (e, t) => {
|
|
42
|
+
if (t instanceof FormData) {
|
|
43
|
+
let { "content-type": t, ...n } = e;
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
return typeof t == "object" && t && !s(t) && !("content-type" in e) ? {
|
|
47
|
+
...e,
|
|
48
|
+
"content-type": "application/json"
|
|
49
|
+
} : e;
|
|
50
|
+
}, u = (e, t, n) => e === "GET" || e === "HEAD" || t == null ? void 0 : s(t) ? t : n["content-type"] === "application/json" ? JSON.stringify(t) : t, d = {
|
|
51
|
+
isNativeBody: s,
|
|
52
|
+
normalizeHeaders: c,
|
|
53
|
+
prepare: (e, t, n) => {
|
|
54
|
+
let r = l(c({
|
|
55
|
+
...n.headers,
|
|
56
|
+
...t.headers ?? {}
|
|
57
|
+
}), t.body);
|
|
58
|
+
return {
|
|
59
|
+
method: e,
|
|
60
|
+
headers: r,
|
|
61
|
+
credentials: t.credentials ?? n.credentials,
|
|
62
|
+
body: u(e, t.body, r)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}, f = {
|
|
66
|
+
json: (e) => e.json(),
|
|
67
|
+
text: (e) => e.text(),
|
|
68
|
+
blob: (e) => e.blob(),
|
|
69
|
+
arrayBuffer: (e) => e.arrayBuffer(),
|
|
70
|
+
formData: (e) => e.formData()
|
|
71
|
+
}, p = (e) => Object.prototype.hasOwnProperty.call(f, e) ? n.of(f[e]) : n.empty(), m = (e) => {
|
|
72
|
+
try {
|
|
73
|
+
return Object.fromEntries(e.headers.entries());
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}, h = (e, t, n) => new o(n.status, a.describe(e), null, t, m(n), "response", e), g = (e, t, n) => new o(e.status, e.statusText, t, n, m(e)), _ = {
|
|
78
|
+
headers: m,
|
|
79
|
+
error: h,
|
|
80
|
+
failure: g,
|
|
81
|
+
read: (t, n, r) => e.from(p(n), () => h(`Unsupported response type: ${n}`, r, t)).flatMap((n) => e.lift(() => t.status === 204 || t.status === 205 ? null : n(t), (e) => h(e, r, t))),
|
|
82
|
+
settle: (t, n, r) => t.ok ? e.of(n) : e.fail(g(t, n, r))
|
|
83
|
+
}, v = (e) => typeof e == "object" && !!e && "name" in e && e.name === "AbortError", y = {
|
|
84
|
+
error: (e, t) => e instanceof o ? e : v(e) ? o.aborted(t, e) : o.network(t, e),
|
|
85
|
+
isRetryable: (e) => e.kind === "network" || e.kind === "response" && e.status >= 500
|
|
86
|
+
}, b = { run: (e, t, n) => e.reduceRight((e, t) => (n) => t.intercept(n, e), n)(t) }, x = (e) => {
|
|
4
87
|
let t, n, r, i = [];
|
|
5
88
|
for (let a of e.split("\n")) {
|
|
6
89
|
if (a === "" || a.startsWith(":")) continue;
|
|
@@ -15,11 +98,7 @@ var a = (e) => {
|
|
|
15
98
|
case "id":
|
|
16
99
|
n = s;
|
|
17
100
|
break;
|
|
18
|
-
case "retry":
|
|
19
|
-
let e = Number.parseInt(s, 10);
|
|
20
|
-
Number.isNaN(e) || (r = e);
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
101
|
+
case "retry": /^\d+$/.test(s) && (r = Number.parseInt(s, 10));
|
|
23
102
|
}
|
|
24
103
|
}
|
|
25
104
|
return i.length === 0 ? null : {
|
|
@@ -28,78 +107,84 @@ var a = (e) => {
|
|
|
28
107
|
...n === void 0 ? {} : { id: n },
|
|
29
108
|
...r === void 0 ? {} : { retry: r }
|
|
30
109
|
};
|
|
31
|
-
},
|
|
110
|
+
}, S = class {
|
|
32
111
|
decoder = new TextDecoder("utf-8");
|
|
33
112
|
buffered = "";
|
|
34
113
|
push(e) {
|
|
35
114
|
let t = this.buffered + this.decoder.decode(e, { stream: !0 }), n = !1;
|
|
36
115
|
t.endsWith("\r") && (t = t.slice(0, -1), n = !0), t = t.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
37
116
|
let r = t.split("\n\n"), i = r.pop() ?? "";
|
|
38
|
-
return n && (i += "\r"), this.buffered = i, r.map(
|
|
117
|
+
return n && (i += "\r"), this.buffered = i, r.map(x).filter((e) => e !== null);
|
|
39
118
|
}
|
|
40
119
|
reset() {
|
|
41
120
|
this.decoder = new TextDecoder("utf-8"), this.buffered = "";
|
|
42
121
|
}
|
|
43
|
-
},
|
|
44
|
-
let e = new
|
|
122
|
+
}, C = () => {
|
|
123
|
+
let e = new S();
|
|
45
124
|
return (n) => n.mapAccumulate(null, (n) => [null, t.from(e.push(n))]);
|
|
46
|
-
},
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return r(t);
|
|
57
|
-
}, d = async (e, t, n) => {
|
|
58
|
-
if (e.status === 204 || e.status === 205) return null;
|
|
59
|
-
try {
|
|
60
|
-
switch (t) {
|
|
61
|
-
case "json": return await e.json();
|
|
62
|
-
case "text": return await e.text();
|
|
63
|
-
case "blob": return await e.blob();
|
|
64
|
-
case "arrayBuffer": return await e.arrayBuffer();
|
|
65
|
-
case "formData": return await e.formData();
|
|
66
|
-
default: throw f(`Unsupported response type: ${t}`, n, e);
|
|
67
|
-
}
|
|
68
|
-
} catch (t) {
|
|
69
|
-
throw t instanceof v ? t : f(t, n, e);
|
|
70
|
-
}
|
|
71
|
-
}, f = (e, t, n) => {
|
|
72
|
-
let r = p(n), i = n ? n.body : null, a = e instanceof Error ? e.message : typeof e == "string" ? e : typeof e == "object" && e && "message" in e ? String(e.message) : "An unknown error occurred.";
|
|
73
|
-
return new v(n?.status || 500, a, i, t, r);
|
|
74
|
-
}, p = (e) => {
|
|
75
|
-
if (!e) return {};
|
|
76
|
-
try {
|
|
77
|
-
return Object.fromEntries(e.headers.entries());
|
|
78
|
-
} catch {
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
}, m = (e) => e.status >= 500, h = (t) => e.cancellable((e) => new Promise((n) => {
|
|
82
|
-
let r = setTimeout(n, t), i = () => {
|
|
83
|
-
clearTimeout(r), n();
|
|
84
|
-
};
|
|
85
|
-
e.aborted ? i() : e.addEventListener("abort", i, { once: !0 });
|
|
86
|
-
})), g = class {
|
|
87
|
-
connection = null;
|
|
88
|
-
frames = new o();
|
|
125
|
+
}, w = 100, T = (t) => e.cancellable((e) => new Promise((n) => {
|
|
126
|
+
let r = () => {
|
|
127
|
+
clearTimeout(i), n();
|
|
128
|
+
}, i = setTimeout(() => {
|
|
129
|
+
e.removeEventListener("abort", r), n();
|
|
130
|
+
}, t);
|
|
131
|
+
e.addEventListener("abort", r, { once: !0 });
|
|
132
|
+
})), E = class {
|
|
133
|
+
controller = new AbortController();
|
|
134
|
+
frames = new S();
|
|
89
135
|
pending = [];
|
|
90
136
|
lastEventId = void 0;
|
|
91
137
|
retryMs;
|
|
92
|
-
|
|
93
|
-
|
|
138
|
+
connection = null;
|
|
139
|
+
unlinkParent;
|
|
140
|
+
constructor(e, t) {
|
|
94
141
|
this.retryMs = e;
|
|
142
|
+
let n = () => this.controller.abort(t.reason);
|
|
143
|
+
t.addEventListener("abort", n, { once: !0 }), this.unlinkParent = () => t.removeEventListener("abort", n);
|
|
144
|
+
}
|
|
145
|
+
get reader() {
|
|
146
|
+
return this.connection?.reader ?? null;
|
|
147
|
+
}
|
|
148
|
+
attach(e) {
|
|
149
|
+
let t = () => e.cancel().catch(() => {});
|
|
150
|
+
this.controller.signal.addEventListener("abort", t, { once: !0 }), this.connection = {
|
|
151
|
+
reader: e,
|
|
152
|
+
unlink: () => this.controller.signal.removeEventListener("abort", t)
|
|
153
|
+
}, this.frames.reset();
|
|
95
154
|
}
|
|
96
155
|
closeConnection() {
|
|
97
|
-
this.connection &&= (this.connection.
|
|
156
|
+
this.connection &&= (this.connection.unlink(), this.connection.reader.cancel().catch(() => {}), null);
|
|
98
157
|
}
|
|
99
158
|
close() {
|
|
100
|
-
this.closeConnection();
|
|
159
|
+
this.closeConnection(), this.unlinkParent(), this.controller.abort();
|
|
101
160
|
}
|
|
102
|
-
},
|
|
161
|
+
}, D = { stream: (t) => {
|
|
162
|
+
let { url: r, headers: a, defaultHeaders: o, credentials: s, interceptors: c, reconnect: l, retry: u } = t, f = (t) => {
|
|
163
|
+
let n = d.prepare("GET", {
|
|
164
|
+
headers: {
|
|
165
|
+
...a,
|
|
166
|
+
accept: "text/event-stream",
|
|
167
|
+
...t.lastEventId === void 0 ? {} : { "last-event-id": t.lastEventId }
|
|
168
|
+
},
|
|
169
|
+
credentials: s
|
|
170
|
+
}, {
|
|
171
|
+
headers: o,
|
|
172
|
+
credentials: s
|
|
173
|
+
});
|
|
174
|
+
return e.lift(() => b.run(c, {
|
|
175
|
+
...n,
|
|
176
|
+
signal: t.controller.signal
|
|
177
|
+
}, (e) => fetch(r, e)), (e) => y.error(e, r)).flatMap((t) => t.ok ? e.of(t) : _.read(t, "text", r).flatMapErr(() => e.of(null)).flatMap((n) => e.fail(_.failure(t, n, r)))).flatMap((t) => t.body === null ? e.fail(_.error("Response has no body", r, t)) : e.of(t.body.getReader()));
|
|
178
|
+
}, p = (t) => e.lift(() => t.read(), (e) => y.error(e, r)), m = e.of(n.empty()), h = (e) => T(e.retryMs).flatMap(() => g(e)), g = (t) => {
|
|
179
|
+
if (t.pending.length > 0) {
|
|
180
|
+
let r = t.pending.shift();
|
|
181
|
+
return r.id !== void 0 && (t.lastEventId = r.id), r.retry !== void 0 && (t.retryMs = Math.max(r.retry, w)), e.of(n.of([r, t]));
|
|
182
|
+
}
|
|
183
|
+
let r = t.reader;
|
|
184
|
+
return r === null ? f(t).foldM((n) => l && y.isRetryable(n) ? h(t) : e.fail(n), (e) => (t.attach(e), g(t))) : p(r).foldM((n) => (t.closeConnection(), l && y.isRetryable(n) ? h(t) : e.fail(n)), (e) => e.done ? (t.closeConnection(), l ? h(t) : m) : (t.pending.push(...t.frames.push(e.value)), g(t)));
|
|
185
|
+
};
|
|
186
|
+
return i.bracket(e.cancellable((e) => new E(u, e)), (e) => i.unfoldEval(e, g), (t) => e.lift(() => t.close()));
|
|
187
|
+
} }, O = class {
|
|
103
188
|
interceptors;
|
|
104
189
|
baseUrl;
|
|
105
190
|
defaultHeaders;
|
|
@@ -135,45 +220,16 @@ var a = (e) => {
|
|
|
135
220
|
fetch(e, t, n = {}) {
|
|
136
221
|
return this.request(e, t, n);
|
|
137
222
|
}
|
|
138
|
-
sse(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
...t === void 0 ? {} : { "last-event-id": t }
|
|
149
|
-
}),
|
|
150
|
-
credentials: _,
|
|
151
|
-
signal: n.signal
|
|
152
|
-
}, i = await u(y, r, (e) => fetch(s, e));
|
|
153
|
-
if (!i.ok) {
|
|
154
|
-
let e = await d(i, "text", s).catch(() => null);
|
|
155
|
-
throw new v(i.status, i.statusText, e, s, p(i));
|
|
156
|
-
}
|
|
157
|
-
if (!i.body) throw new v(500, "Response has no body", null, s, p(i));
|
|
158
|
-
return {
|
|
159
|
-
reader: i.body.getReader(),
|
|
160
|
-
controller: n
|
|
161
|
-
};
|
|
162
|
-
}, (e) => e instanceof v ? e : f(e, s)), S = (t) => e.cancellable((e) => {
|
|
163
|
-
let n = () => t.cancel().catch(() => {});
|
|
164
|
-
return e.aborted ? n() : e.addEventListener("abort", n, { once: !0 }), t.read();
|
|
165
|
-
}, (e) => e instanceof v ? e : f(e, s)), C = (t) => {
|
|
166
|
-
if (t.pending.length > 0) {
|
|
167
|
-
let r = t.pending.shift();
|
|
168
|
-
return r.id !== void 0 && (t.lastEventId = r.id), r.retry !== void 0 && (t.retryMs = r.retry), e.of(n.of([r, t]));
|
|
169
|
-
}
|
|
170
|
-
if (t.connection === null) {
|
|
171
|
-
let n = x(t.lastEventId).foldM((n) => a && m(n) ? (t.shouldDelay = !0, C(t)) : e.fail(n), (e) => (t.connection = e, t.frames.reset(), t.shouldDelay = !1, C(t)));
|
|
172
|
-
return t.shouldDelay ? h(t.retryMs).flatMap(() => n) : n;
|
|
173
|
-
}
|
|
174
|
-
return S(t.connection.reader).foldM((n) => (t.closeConnection(), a && m(n) ? (t.shouldDelay = !0, C(t)) : e.fail(n)), (r) => r.done ? (t.closeConnection(), a ? (t.shouldDelay = !0, C(t)) : e.of(n.empty())) : (r.value !== void 0 && t.pending.push(...t.frames.push(r.value)), C(t)));
|
|
175
|
-
};
|
|
176
|
-
return i.bracket(e.lift(() => new g(o)), (e) => i.unfoldEval(e, C), (t) => e.lift(() => t.close()));
|
|
223
|
+
sse(e, t = {}) {
|
|
224
|
+
return D.stream({
|
|
225
|
+
url: this.resolveUrl(e),
|
|
226
|
+
headers: t.headers ?? {},
|
|
227
|
+
defaultHeaders: this.defaultHeaders,
|
|
228
|
+
credentials: t.credentials ?? this.defaultCredentials,
|
|
229
|
+
interceptors: this.interceptors,
|
|
230
|
+
reconnect: t.reconnect ?? !0,
|
|
231
|
+
retry: t.retry ?? 3e3
|
|
232
|
+
});
|
|
177
233
|
}
|
|
178
234
|
resolveUrl(e) {
|
|
179
235
|
if (!this.baseUrl) return e;
|
|
@@ -184,46 +240,17 @@ var a = (e) => {
|
|
|
184
240
|
}
|
|
185
241
|
}
|
|
186
242
|
request(t, n, r = {}) {
|
|
187
|
-
let i = this.resolveUrl(t), a = r.timeout ?? this.defaultTimeout
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
let e = {
|
|
197
|
-
method: n,
|
|
198
|
-
headers: g,
|
|
199
|
-
credentials: f,
|
|
200
|
-
body: _,
|
|
201
|
-
signal: x
|
|
202
|
-
}, t = await u(this.interceptors, e, (e) => fetch(i, e));
|
|
203
|
-
if (!t.ok) {
|
|
204
|
-
let e = await d(t, s, i), n = p(t);
|
|
205
|
-
throw new v(t.status, t.statusText, e, i, n);
|
|
206
|
-
}
|
|
207
|
-
return m === "response" ? t : h(await d(t, s, i));
|
|
208
|
-
} finally {
|
|
209
|
-
b != null && clearTimeout(b);
|
|
210
|
-
}
|
|
211
|
-
}, (e) => e instanceof v ? e : f(e, i));
|
|
212
|
-
}
|
|
213
|
-
}, v = class extends Error {
|
|
214
|
-
status;
|
|
215
|
-
rawMessage;
|
|
216
|
-
body;
|
|
217
|
-
url;
|
|
218
|
-
headers;
|
|
219
|
-
constructor(e, t, n, r, i) {
|
|
220
|
-
super(`Request to '${r}' failed with status ${e} and message: ${t}.`), this.name = "HttpError", this.status = e, this.rawMessage = t, this.body = n, this.url = r, this.headers = i;
|
|
221
|
-
}
|
|
222
|
-
bodyAs(e) {
|
|
223
|
-
return e(this.body) ? r.of(this.body) : n.empty();
|
|
243
|
+
let i = this.resolveUrl(t), a = r.timeout ?? this.defaultTimeout, { responseType: s = "json", observe: c = "body", transform: l = (e) => e } = r, u = {
|
|
244
|
+
headers: this.defaultHeaders,
|
|
245
|
+
credentials: this.defaultCredentials
|
|
246
|
+
}, f = e.lift(() => d.prepare(n, r, u), (e) => o.request(i, e)), p = (t) => e.cancellable((e) => b.run(this.interceptors, {
|
|
247
|
+
...t,
|
|
248
|
+
signal: e
|
|
249
|
+
}, (e) => fetch(i, e)), (e) => y.error(e, i)), m = (t) => _.read(t, s, i).flatMap((e) => _.settle(t, e, i)).flatMap((n) => e.lift(() => l(n), (e) => _.error(e, i, t))), h = f.flatMap(p).flatMap((t) => t.ok && c === "response" ? e.of(t) : m(t));
|
|
250
|
+
return a == null ? h : h.timeout(a, () => o.timeout(i, a));
|
|
224
251
|
}
|
|
225
252
|
};
|
|
226
253
|
//#endregion
|
|
227
|
-
export {
|
|
254
|
+
export { O as HttpClient, o as HttpError, C as decodeEvents };
|
|
228
255
|
|
|
229
256
|
//# sourceMappingURL=monadyssey-fetch.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.mjs","names":[],"sources":["../src/sse.ts","../src/http-client.ts"],"sourcesContent":["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/**\n * Parses a single event block (the text between two blank lines) into a {@link ServerSentEvent}.\n * Returns `null` when the block dispatches no event (e.g. a comment-only or field-only block with\n * no `data:`), matching the event-stream dispatch rules.\n */\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 const parsed = Number.parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\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 * Incrementally reassembles a byte stream into complete {@link ServerSentEvent}s. Feed it chunks\n * with {@link push}; it buffers partial input, so events split across byte boundaries — and\n * multi-byte characters split mid-character — are reassembled correctly. Comment/heartbeat blocks\n * and an unterminated trailing block yield nothing.\n *\n * Stateful and single-connection: call {@link reset} (or use a fresh instance) when a new\n * connection begins, so an incomplete event from a dropped connection is discarded.\n *\n * Package-internal — not part of the public API.\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n /** Decodes and frames a chunk, returning any events completed by it. */\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 /** Discards buffered partial input, e.g. before reconnecting. */\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, Some, Stream } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType, SseOptions } from \"./options\";\nimport { FrameBuffer, ServerSentEvent } from \"./sse\";\n\n/**\n * Returns `true` if the body is a type that the fetch API knows how to send natively.\n * These types must NOT be JSON.stringified and must NOT have a Content-Type header auto-set\n * (the browser handles multipart boundaries for FormData, etc.).\n */\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\n/**\n * Normalizes header keys to lowercase for case-insensitive comparison.\n * HTTP header names are case-insensitive per RFC 7230.\n */\nconst normalizeHeaders = (headers: Record<string, string>): Record<string, string> => {\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n result[key.toLowerCase()] = value;\n }\n }\n return result;\n};\n\n/**\n * Builds the interceptor chain as a pure function. No global state.\n * Interceptors are applied in registration order — first registered is outermost.\n */\nconst runInterceptors = (\n interceptors: readonly HttpInterceptor[],\n req: RequestInit,\n fn: (req: RequestInit) => Promise<Response>\n): Promise<Response> => {\n let next = fn;\n for (const interceptor of [...interceptors].reverse()) {\n const currentNext = next;\n next = (r: RequestInit) => interceptor.intercept(r, currentNext);\n }\n return next(req);\n};\n\nconst parseBody = async (response: Response, responseType: ResponseType, url: string): Promise<unknown> => {\n if (response.status === 204 || response.status === 205) {\n return null;\n }\n try {\n switch (responseType) {\n case \"json\":\n return await response.json();\n case \"text\":\n return await response.text();\n case \"blob\":\n return await response.blob();\n case \"arrayBuffer\":\n return await response.arrayBuffer();\n case \"formData\":\n return await response.formData();\n default:\n throw toHttpError(`Unsupported response type: ${responseType}`, url, response);\n }\n } catch (e: unknown) {\n if (e instanceof HttpError) throw e;\n throw toHttpError(e, url, response);\n }\n};\n\nconst toHttpError = (e: unknown, uri: string, response?: Response): HttpError => {\n const headers = extractHeadersFrom(response);\n const body = response ? response.body : null;\n\n const message =\n e instanceof Error\n ? e.message\n : typeof e === \"string\"\n ? e\n : typeof e === \"object\" && e !== null && \"message\" in e\n ? String((e as any).message)\n : \"An unknown error occurred.\";\n\n return new HttpError(response?.status || 500, message, body, uri, headers);\n};\n\nconst extractHeadersFrom = (response?: Response): Record<string, string> => {\n if (!response) return {};\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** A retryable failure is a network error or a server error; `4xx` responses are terminal. */\nconst isRetryable = (error: HttpError): boolean => error.status >= 500;\n\n/** A cancellable delay — an abort resolves it immediately so a pending reconnect wait is interruptible. */\nconst delay = <E = never>(ms: number): IO<E, void> =>\n IO.cancellable<E, void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/** A live SSE connection: the body reader and the controller that aborts it. */\ninterface Connection {\n readonly reader: ReadableStreamDefaultReader<Uint8Array>;\n readonly controller: AbortController;\n}\n\n/** Mutable state for a single `sse` run, carried across reconnections. */\nclass SseSession {\n connection: Connection | null = null;\n readonly frames = new FrameBuffer();\n readonly pending: ServerSentEvent[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n shouldDelay = false;\n\n constructor(retryMs: number) {\n this.retryMs = retryMs;\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.controller.abort();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n }\n}\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 const reconnect = options.reconnect ?? true;\n const defaultRetry = options.retry ?? 3000;\n const resolvedUrl = this.resolveUrl(uri);\n const requestHeaders = options.headers ?? {};\n const credentials = options.credentials ?? this.defaultCredentials;\n const interceptors = this.interceptors;\n const defaultHeaders = this.defaultHeaders;\n\n const open = (lastEventId: string | undefined): IO<HttpError, Connection> =>\n IO.lift<HttpError, Connection>(\n async (signal?: AbortSignal) => {\n const controller = new AbortController();\n if (signal) {\n if (signal.aborted) controller.abort(signal.reason);\n else signal.addEventListener(\"abort\", () => controller.abort(signal.reason), { once: true });\n }\n const headers = normalizeHeaders({\n ...defaultHeaders,\n ...requestHeaders,\n accept: \"text/event-stream\",\n ...(lastEventId !== undefined ? { \"last-event-id\": lastEventId } : {}),\n });\n const requestInit: RequestInit = { method: \"GET\", headers, credentials, signal: controller.signal };\n const response = await runInterceptors(interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n if (!response.ok) {\n const body = await parseBody(response, \"text\", resolvedUrl).catch(() => null);\n throw new HttpError(response.status, response.statusText, body, resolvedUrl, extractHeadersFrom(response));\n }\n if (!response.body) {\n throw new HttpError(500, \"Response has no body\", null, resolvedUrl, extractHeadersFrom(response));\n }\n return { reader: response.body.getReader(), controller };\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n const readChunk = (\n reader: ReadableStreamDefaultReader<Uint8Array>\n ): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.cancellable<HttpError, ReadableStreamReadResult<Uint8Array>>(\n (signal: AbortSignal) => {\n const onAbort = () => reader.cancel().catch(() => {});\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n return reader.read();\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\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 = event.retry;\n return IO.of(Option.of<[ServerSentEvent, SseSession]>([event, session]));\n }\n\n if (session.connection === null) {\n const connect = open(session.lastEventId).foldM<HttpError, EventStep>(\n (error) => {\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (connection) => {\n session.connection = connection;\n session.frames.reset();\n session.shouldDelay = false;\n return step(session);\n }\n );\n return session.shouldDelay ? delay<HttpError>(session.retryMs).flatMap(() => connect) : connect;\n }\n\n return readChunk(session.connection.reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n if (reconnect) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.of(Option.empty<[ServerSentEvent, SseSession]>());\n }\n if (result.value !== undefined) {\n session.pending.push(...session.frames.push(result.value));\n }\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.lift(() => new SseSession(defaultRetry)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.lift(() => session.close())\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 resolvedUrl = this.resolveUrl(uri);\n const timeout = options.timeout ?? this.defaultTimeout;\n\n return IO.cancellable<HttpError, Response | A | null>(\n async (signal: AbortSignal) => {\n const {\n headers = {},\n body,\n responseType = \"json\",\n credentials = this.defaultCredentials,\n observe = \"body\",\n transform = (data: unknown) => data as A,\n } = options;\n\n const mergedHeaders = normalizeHeaders({ ...this.defaultHeaders, ...headers });\n\n const shouldAutoJson =\n body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in mergedHeaders);\n\n if (shouldAutoJson) {\n mergedHeaders[\"content-type\"] = \"application/json\";\n }\n\n if (body instanceof FormData) {\n delete mergedHeaders[\"content-type\"];\n }\n\n const serializedBody =\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? body\n : mergedHeaders[\"content-type\"] === \"application/json\"\n ? JSON.stringify(body)\n : body;\n\n let controller: AbortController | undefined;\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const fetchSignal = (() => {\n if (timeout != null) {\n controller = new AbortController();\n const onParentAbort = () => controller!.abort(signal.reason);\n signal.addEventListener(\"abort\", onParentAbort, { once: true });\n timeoutId = setTimeout(\n () => controller!.abort(new DOMException(\"Request timed out\", \"TimeoutError\")),\n timeout\n );\n return controller.signal;\n }\n return signal;\n })();\n\n try {\n const requestInit: RequestInit = {\n method,\n headers: mergedHeaders,\n credentials,\n body: serializedBody as BodyInit | undefined,\n signal: fetchSignal,\n };\n\n const response = await runInterceptors(this.interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n\n if (!response.ok) {\n const rb = await parseBody(response, responseType, resolvedUrl);\n const respHeaders = extractHeadersFrom(response);\n throw new HttpError(response.status, response.statusText, rb, resolvedUrl, respHeaders);\n }\n\n if (observe === \"response\") {\n return response;\n }\n\n const rb = await parseBody(response, responseType, resolvedUrl);\n return transform(rb);\n } finally {\n if (timeoutId != null) clearTimeout(timeoutId);\n }\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n }\n}\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: HTTP status code,\n * raw error message, response body, request URL, and response headers.\n */\nexport class HttpError extends Error {\n public readonly status: number;\n public readonly rawMessage: string;\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n\n constructor(status: number, rawMessage: string, body: unknown, url: string, headers?: Record<string, string>) {\n super(`Request to '${url}' failed with status ${status} and message: ${rawMessage}.`);\n this.name = \"HttpError\";\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\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"],"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, 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 +1,8 @@
|
|
|
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=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
|
|
3
|
-
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},
|
|
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
4
|
`).replace(/\r/g,`
|
|
5
|
-
`);let
|
|
5
|
+
`);let r=t.split(`
|
|
6
6
|
|
|
7
|
-
`),
|
|
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
8
|
//# sourceMappingURL=monadyssey-fetch.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.umd.js","names":[],"sources":["../src/sse.ts","../src/http-client.ts"],"sourcesContent":["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/**\n * Parses a single event block (the text between two blank lines) into a {@link ServerSentEvent}.\n * Returns `null` when the block dispatches no event (e.g. a comment-only or field-only block with\n * no `data:`), matching the event-stream dispatch rules.\n */\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 const parsed = Number.parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\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 * Incrementally reassembles a byte stream into complete {@link ServerSentEvent}s. Feed it chunks\n * with {@link push}; it buffers partial input, so events split across byte boundaries — and\n * multi-byte characters split mid-character — are reassembled correctly. Comment/heartbeat blocks\n * and an unterminated trailing block yield nothing.\n *\n * Stateful and single-connection: call {@link reset} (or use a fresh instance) when a new\n * connection begins, so an incomplete event from a dropped connection is discarded.\n *\n * Package-internal — not part of the public API.\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n /** Decodes and frames a chunk, returning any events completed by it. */\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 /** Discards buffered partial input, e.g. before reconnecting. */\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, Some, Stream } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType, SseOptions } from \"./options\";\nimport { FrameBuffer, ServerSentEvent } from \"./sse\";\n\n/**\n * Returns `true` if the body is a type that the fetch API knows how to send natively.\n * These types must NOT be JSON.stringified and must NOT have a Content-Type header auto-set\n * (the browser handles multipart boundaries for FormData, etc.).\n */\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\n/**\n * Normalizes header keys to lowercase for case-insensitive comparison.\n * HTTP header names are case-insensitive per RFC 7230.\n */\nconst normalizeHeaders = (headers: Record<string, string>): Record<string, string> => {\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n result[key.toLowerCase()] = value;\n }\n }\n return result;\n};\n\n/**\n * Builds the interceptor chain as a pure function. No global state.\n * Interceptors are applied in registration order — first registered is outermost.\n */\nconst runInterceptors = (\n interceptors: readonly HttpInterceptor[],\n req: RequestInit,\n fn: (req: RequestInit) => Promise<Response>\n): Promise<Response> => {\n let next = fn;\n for (const interceptor of [...interceptors].reverse()) {\n const currentNext = next;\n next = (r: RequestInit) => interceptor.intercept(r, currentNext);\n }\n return next(req);\n};\n\nconst parseBody = async (response: Response, responseType: ResponseType, url: string): Promise<unknown> => {\n if (response.status === 204 || response.status === 205) {\n return null;\n }\n try {\n switch (responseType) {\n case \"json\":\n return await response.json();\n case \"text\":\n return await response.text();\n case \"blob\":\n return await response.blob();\n case \"arrayBuffer\":\n return await response.arrayBuffer();\n case \"formData\":\n return await response.formData();\n default:\n throw toHttpError(`Unsupported response type: ${responseType}`, url, response);\n }\n } catch (e: unknown) {\n if (e instanceof HttpError) throw e;\n throw toHttpError(e, url, response);\n }\n};\n\nconst toHttpError = (e: unknown, uri: string, response?: Response): HttpError => {\n const headers = extractHeadersFrom(response);\n const body = response ? response.body : null;\n\n const message =\n e instanceof Error\n ? e.message\n : typeof e === \"string\"\n ? e\n : typeof e === \"object\" && e !== null && \"message\" in e\n ? String((e as any).message)\n : \"An unknown error occurred.\";\n\n return new HttpError(response?.status || 500, message, body, uri, headers);\n};\n\nconst extractHeadersFrom = (response?: Response): Record<string, string> => {\n if (!response) return {};\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** A retryable failure is a network error or a server error; `4xx` responses are terminal. */\nconst isRetryable = (error: HttpError): boolean => error.status >= 500;\n\n/** A cancellable delay — an abort resolves it immediately so a pending reconnect wait is interruptible. */\nconst delay = <E = never>(ms: number): IO<E, void> =>\n IO.cancellable<E, void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/** A live SSE connection: the body reader and the controller that aborts it. */\ninterface Connection {\n readonly reader: ReadableStreamDefaultReader<Uint8Array>;\n readonly controller: AbortController;\n}\n\n/** Mutable state for a single `sse` run, carried across reconnections. */\nclass SseSession {\n connection: Connection | null = null;\n readonly frames = new FrameBuffer();\n readonly pending: ServerSentEvent[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n shouldDelay = false;\n\n constructor(retryMs: number) {\n this.retryMs = retryMs;\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.controller.abort();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n }\n}\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 const reconnect = options.reconnect ?? true;\n const defaultRetry = options.retry ?? 3000;\n const resolvedUrl = this.resolveUrl(uri);\n const requestHeaders = options.headers ?? {};\n const credentials = options.credentials ?? this.defaultCredentials;\n const interceptors = this.interceptors;\n const defaultHeaders = this.defaultHeaders;\n\n const open = (lastEventId: string | undefined): IO<HttpError, Connection> =>\n IO.lift<HttpError, Connection>(\n async (signal?: AbortSignal) => {\n const controller = new AbortController();\n if (signal) {\n if (signal.aborted) controller.abort(signal.reason);\n else signal.addEventListener(\"abort\", () => controller.abort(signal.reason), { once: true });\n }\n const headers = normalizeHeaders({\n ...defaultHeaders,\n ...requestHeaders,\n accept: \"text/event-stream\",\n ...(lastEventId !== undefined ? { \"last-event-id\": lastEventId } : {}),\n });\n const requestInit: RequestInit = { method: \"GET\", headers, credentials, signal: controller.signal };\n const response = await runInterceptors(interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n if (!response.ok) {\n const body = await parseBody(response, \"text\", resolvedUrl).catch(() => null);\n throw new HttpError(response.status, response.statusText, body, resolvedUrl, extractHeadersFrom(response));\n }\n if (!response.body) {\n throw new HttpError(500, \"Response has no body\", null, resolvedUrl, extractHeadersFrom(response));\n }\n return { reader: response.body.getReader(), controller };\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n const readChunk = (\n reader: ReadableStreamDefaultReader<Uint8Array>\n ): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.cancellable<HttpError, ReadableStreamReadResult<Uint8Array>>(\n (signal: AbortSignal) => {\n const onAbort = () => reader.cancel().catch(() => {});\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n return reader.read();\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\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 = event.retry;\n return IO.of(Option.of<[ServerSentEvent, SseSession]>([event, session]));\n }\n\n if (session.connection === null) {\n const connect = open(session.lastEventId).foldM<HttpError, EventStep>(\n (error) => {\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (connection) => {\n session.connection = connection;\n session.frames.reset();\n session.shouldDelay = false;\n return step(session);\n }\n );\n return session.shouldDelay ? delay<HttpError>(session.retryMs).flatMap(() => connect) : connect;\n }\n\n return readChunk(session.connection.reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n if (reconnect && isRetryable(error)) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.fail<HttpError, EventStep>(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n if (reconnect) {\n session.shouldDelay = true;\n return step(session);\n }\n return IO.of(Option.empty<[ServerSentEvent, SseSession]>());\n }\n if (result.value !== undefined) {\n session.pending.push(...session.frames.push(result.value));\n }\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.lift(() => new SseSession(defaultRetry)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.lift(() => session.close())\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 resolvedUrl = this.resolveUrl(uri);\n const timeout = options.timeout ?? this.defaultTimeout;\n\n return IO.cancellable<HttpError, Response | A | null>(\n async (signal: AbortSignal) => {\n const {\n headers = {},\n body,\n responseType = \"json\",\n credentials = this.defaultCredentials,\n observe = \"body\",\n transform = (data: unknown) => data as A,\n } = options;\n\n const mergedHeaders = normalizeHeaders({ ...this.defaultHeaders, ...headers });\n\n const shouldAutoJson =\n body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in mergedHeaders);\n\n if (shouldAutoJson) {\n mergedHeaders[\"content-type\"] = \"application/json\";\n }\n\n if (body instanceof FormData) {\n delete mergedHeaders[\"content-type\"];\n }\n\n const serializedBody =\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? body\n : mergedHeaders[\"content-type\"] === \"application/json\"\n ? JSON.stringify(body)\n : body;\n\n let controller: AbortController | undefined;\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const fetchSignal = (() => {\n if (timeout != null) {\n controller = new AbortController();\n const onParentAbort = () => controller!.abort(signal.reason);\n signal.addEventListener(\"abort\", onParentAbort, { once: true });\n timeoutId = setTimeout(\n () => controller!.abort(new DOMException(\"Request timed out\", \"TimeoutError\")),\n timeout\n );\n return controller.signal;\n }\n return signal;\n })();\n\n try {\n const requestInit: RequestInit = {\n method,\n headers: mergedHeaders,\n credentials,\n body: serializedBody as BodyInit | undefined,\n signal: fetchSignal,\n };\n\n const response = await runInterceptors(this.interceptors, requestInit, (req) => fetch(resolvedUrl, req));\n\n if (!response.ok) {\n const rb = await parseBody(response, responseType, resolvedUrl);\n const respHeaders = extractHeadersFrom(response);\n throw new HttpError(response.status, response.statusText, rb, resolvedUrl, respHeaders);\n }\n\n if (observe === \"response\") {\n return response;\n }\n\n const rb = await parseBody(response, responseType, resolvedUrl);\n return transform(rb);\n } finally {\n if (timeoutId != null) clearTimeout(timeoutId);\n }\n },\n (e: unknown) => (e instanceof HttpError ? e : toHttpError(e, resolvedUrl))\n );\n }\n}\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: HTTP status code,\n * raw error message, response body, request URL, and response headers.\n */\nexport class HttpError extends Error {\n public readonly status: number;\n public readonly rawMessage: string;\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n\n constructor(status: number, rawMessage: string, body: unknown, url: string, headers?: Record<string, string>) {\n super(`Request to '${url}' failed with status ${status} and message: ${rawMessage}.`);\n this.name = \"HttpError\";\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"monadyssey-fetch.umd.js","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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openstage/monadyssey-fetch",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://
|
|
11
|
+
"url": "https://gitlab.com/open-stage/monadyssey.git"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"functional programming",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
"prettier:fix": "npm run prettier -- --write"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@openstage/monadyssey-core": "^3.0.0-beta.
|
|
42
|
+
"@openstage/monadyssey-core": "^3.0.0-beta.7"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/readme.md
CHANGED
|
@@ -111,8 +111,11 @@ await api.fetch<{ message: string }>("/custom", "OPTIONS", {
|
|
|
111
111
|
|
|
112
112
|
Errors are encapsulated in the `HttpError` type, which includes:
|
|
113
113
|
|
|
114
|
-
* `
|
|
115
|
-
* `
|
|
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 did, `"timeout"` when the `timeout` option fired, `"aborted"` when the request was aborted outside IO cancellation
|
|
115
|
+
* `status`: The HTTP status code, or `0` when no response arrived
|
|
116
|
+
* `message`: Formatted error message containing the URL and, for a response, the status and raw message
|
|
117
|
+
* `cause`: The underlying exception, if any
|
|
118
|
+
* `timeoutMs`: The timeout that fired, in milliseconds; set only when `kind` is `"timeout"`
|
|
116
119
|
* `rawMessage`: The raw error description
|
|
117
120
|
* `body`: The response body if available, typed `unknown`. Narrow it safely with `error.bodyAs(guard)`, which returns `Option<T>`
|
|
118
121
|
* `url`: The request URL
|