@openstage/monadyssey-fetch 3.0.0-beta.3 → 3.0.0-beta.4
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 +7 -1
- package/dist/monadyssey-fetch.cjs.map +1 -1
- package/dist/monadyssey-fetch.d.ts +70 -0
- package/dist/monadyssey-fetch.mjs +133 -28
- package/dist/monadyssey-fetch.mjs.map +1 -1
- package/dist/monadyssey-fetch.umd.js +7 -1
- package/dist/monadyssey-fetch.umd.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +1 -1
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@openstage/monadyssey-core`);var t=e=>
|
|
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`:{let e=Number.parseInt(s,10);Number.isNaN(e)||(r=e);break}default:break}}return i.length===0?null:{event:t??`message`,data:i.join(`
|
|
3
|
+
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},n=class{decoder=new TextDecoder(`utf-8`);buffered=``;push(e){let n=this.buffered+this.decoder.decode(e,{stream:!0}),r=!1;n.endsWith(`\r`)&&(n=n.slice(0,-1),r=!0),n=n.replace(/\r\n/g,`
|
|
4
|
+
`).replace(/\r/g,`
|
|
5
|
+
`);let i=n.split(`
|
|
6
|
+
|
|
7
|
+
`),a=i.pop()??``;return r&&(a+=`\r`),this.buffered=a,i.map(t).filter(e=>e!==null)}reset(){this.decoder=new TextDecoder(`utf-8`),this.buffered=``}},r=()=>{let t=new n;return n=>n.mapAccumulate(null,n=>[null,e.List.from(t.push(n))])},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=>{let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n.toLowerCase()]=r);return t},o=(e,t,n)=>{let r=n;for(let t of[...e].reverse()){let e=r;r=n=>t.intercept(n,e)}return r(t)},s=async(e,t,n)=>{if(e.status===204||e.status===205)return null;try{switch(t){case`json`:return await e.json();case`text`:return await e.text();case`blob`:return await e.blob();case`arrayBuffer`:return await e.arrayBuffer();case`formData`:return await e.formData();default:throw c(`Unsupported response type: ${t}`,n,e)}}catch(t){throw t instanceof m?t:c(t,n,e)}},c=(e,t,n)=>{let r=l(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.`;return new m(n?.status||500,a,i,t,r)},l=e=>{if(!e)return{};try{return Object.fromEntries(e.headers.entries())}catch{return{}}},u=e=>e.status>=500,d=t=>e.IO.cancellable(e=>new Promise(n=>{let r=setTimeout(n,t),i=()=>{clearTimeout(r),n()};e.aborted?i():e.addEventListener(`abort`,i,{once:!0})})),f=class{connection=null;frames=new n;pending=[];lastEventId=void 0;retryMs;shouldDelay=!1;constructor(e){this.retryMs=e}closeConnection(){this.connection&&=(this.connection.controller.abort(),this.connection.reader.cancel().catch(()=>{}),null)}close(){this.closeConnection()}},p=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(t,n={}){let r=n.reconnect??!0,i=n.retry??3e3,p=this.resolveUrl(t),h=n.headers??{},g=n.credentials??this.defaultCredentials,_=this.interceptors,v=this.defaultHeaders,y=t=>e.IO.lift(async e=>{let n=new AbortController;e&&(e.aborted?n.abort(e.reason):e.addEventListener(`abort`,()=>n.abort(e.reason),{once:!0}));let r=await o(_,{method:`GET`,headers:a({...v,...h,accept:`text/event-stream`,...t===void 0?{}:{"last-event-id":t}}),credentials:g,signal:n.signal},e=>fetch(p,e));if(!r.ok){let e=await s(r,`text`,p).catch(()=>null);throw new m(r.status,r.statusText,e,p,l(r))}if(!r.body)throw new m(500,`Response has no body`,null,p,l(r));return{reader:r.body.getReader(),controller:n}},e=>e instanceof m?e:c(e,p)),b=t=>e.IO.cancellable(e=>{let n=()=>t.cancel().catch(()=>{});return e.aborted?n():e.addEventListener(`abort`,n,{once:!0}),t.read()},e=>e instanceof m?e:c(e,p)),x=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=n.retry),e.IO.pure(e.Option.pure([n,t]))}if(t.connection===null){let n=y(t.lastEventId).foldM(n=>r&&u(n)?(t.shouldDelay=!0,x(t)):e.IO.fail(n),e=>(t.connection=e,t.frames.reset(),t.shouldDelay=!1,x(t)));return t.shouldDelay?d(t.retryMs).flatMap(()=>n):n}return b(t.connection.reader).foldM(n=>(t.closeConnection(),r&&u(n)?(t.shouldDelay=!0,x(t)):e.IO.fail(n)),n=>n.done?(t.closeConnection(),r?(t.shouldDelay=!0,x(t)):e.IO.pure(e.None.Instance)):(n.value!==void 0&&t.pending.push(...t.frames.push(n.value)),x(t)))};return e.Stream.bracket(e.IO.lift(()=>new f(i)),t=>e.Stream.unfoldEval(t,x),t=>e.IO.lift(()=>t.close()))}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,n,r={}){let u=this.resolveUrl(t),d=r.timeout??this.defaultTimeout;return e.IO.cancellable(async e=>{let{headers:t={},body:c,responseType:f=`json`,credentials:p=this.defaultCredentials,observe:h=`body`,transform:g=e=>e}=r,_=a({...this.defaultHeaders,...t});typeof c==`object`&&c&&!i(c)&&!(`content-type`in _)&&(_[`content-type`]=`application/json`),c instanceof FormData&&delete _[`content-type`];let v=n===`GET`||n===`HEAD`||c==null?void 0:i(c)?c:_[`content-type`]===`application/json`?JSON.stringify(c):c,y,b,x=d==null?e:(y=new AbortController,e.addEventListener(`abort`,()=>y.abort(e.reason),{once:!0}),b=setTimeout(()=>y.abort(new DOMException(`Request timed out`,`TimeoutError`)),d),y.signal);try{let e={method:n,headers:_,credentials:p,body:v,signal:x},t=await o(this.interceptors,e,e=>fetch(u,e));if(!t.ok){let e=await s(t,f,u),n=l(t);throw new m(t.status,t.statusText,e,u,n)}return h===`response`?t:g(await s(t,f,u))}finally{b!=null&&clearTimeout(b)}},e=>e instanceof m?e:c(e,u))}},m=class extends Error{status;rawMessage;body;url;headers;constructor(e,t,n,r,i){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}bodyAs(t){return t(this.body)?e.Some.pure(this.body):e.None.Instance}};exports.HttpClient=p,exports.HttpError=m,exports.decodeEvents=r;
|
|
2
8
|
//# sourceMappingURL=monadyssey-fetch.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.cjs","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { IO, Option, Some, None } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType } from \"./options\";\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/**\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 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\n }\n}\n"],"mappings":"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"}
|
|
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, None, 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.pure(Option.pure<[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.pure(None.Instance as EventStep);\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, 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { IO } from '@openstage/monadyssey-core';
|
|
2
2
|
import { Option as Option_2 } from '@openstage/monadyssey-core';
|
|
3
|
+
import { Pipe } from '@openstage/monadyssey-core';
|
|
4
|
+
import { Stream } from '@openstage/monadyssey-core';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Specifies the credentials policy for the request.
|
|
@@ -9,6 +11,15 @@ import { Option as Option_2 } from '@openstage/monadyssey-core';
|
|
|
9
11
|
*/
|
|
10
12
|
export declare type Credentials = "omit" | "same-origin" | "include";
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering
|
|
16
|
+
* partial input across chunks. Single-use: each application maintains its own decoding state.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam E - The error type carried by the surrounding stream.
|
|
19
|
+
* @returns A pipe from a byte stream to a stream of parsed events.
|
|
20
|
+
*/
|
|
21
|
+
export declare const decodeEvents: <E>() => Pipe<E, Uint8Array, ServerSentEvent>;
|
|
22
|
+
|
|
12
23
|
/**
|
|
13
24
|
* A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of Promises.
|
|
14
25
|
*
|
|
@@ -131,6 +142,28 @@ export declare class HttpClient {
|
|
|
131
142
|
observe: "response";
|
|
132
143
|
}): IO<HttpError, Response>;
|
|
133
144
|
fetch<A = unknown>(uri: string, method: Method, options?: Options<A>): IO<HttpError, A | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.
|
|
147
|
+
*
|
|
148
|
+
* The request goes through the same pipeline as every other method, so headers (including
|
|
149
|
+
* `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is
|
|
150
|
+
* used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts
|
|
151
|
+
* the connection and releases resources.
|
|
152
|
+
*
|
|
153
|
+
* By default the stream reconnects automatically when the connection ends or fails with a
|
|
154
|
+
* retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers
|
|
155
|
+
* and credentials on each attempt; terminal `4xx` responses (including auth failures) end the
|
|
156
|
+
* stream. See {@link SseOptions} to configure or disable this.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} uri - The URL or path to connect to.
|
|
159
|
+
* @param {SseOptions} [options] - Headers, credentials, and reconnection settings.
|
|
160
|
+
* @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* const events = client.sse("/events", { headers: { Authorization: `Bearer ${token}` } });
|
|
164
|
+
* await events.runForEach((event) => IO.lift(() => handle(event))).unsafeRun();
|
|
165
|
+
*/
|
|
166
|
+
sse(uri: string, options?: SseOptions): Stream<HttpError, ServerSentEvent>;
|
|
134
167
|
private resolveUrl;
|
|
135
168
|
private request;
|
|
136
169
|
}
|
|
@@ -250,4 +283,41 @@ export declare type Options<A = unknown> = {
|
|
|
250
283
|
declare type ResponseType_2 = "json" | "text" | "blob" | "arrayBuffer" | "formData";
|
|
251
284
|
export { ResponseType_2 as ResponseType }
|
|
252
285
|
|
|
286
|
+
/**
|
|
287
|
+
* A single server-sent event parsed from a text/event-stream response.
|
|
288
|
+
*
|
|
289
|
+
* @property id - The event id, if the server sent an `id:` field. Used to resume a stream.
|
|
290
|
+
* @property event - The event type. Defaults to `"message"` when the server sends no `event:` field.
|
|
291
|
+
* @property data - The event payload. Multiple `data:` lines are joined with a newline.
|
|
292
|
+
* @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.
|
|
293
|
+
*/
|
|
294
|
+
export declare interface ServerSentEvent {
|
|
295
|
+
readonly id?: string;
|
|
296
|
+
readonly event: string;
|
|
297
|
+
readonly data: string;
|
|
298
|
+
readonly retry?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Options for opening a server-sent-events stream.
|
|
303
|
+
*/
|
|
304
|
+
export declare type SseOptions = {
|
|
305
|
+
/** Custom headers for the request (e.g. `Authorization`). Merged with the client's default headers. */
|
|
306
|
+
headers?: Record<string, string>;
|
|
307
|
+
/** The credential policy — set `"include"` to send cookies. Defaults to the client-level setting. */
|
|
308
|
+
credentials?: Credentials;
|
|
309
|
+
/**
|
|
310
|
+
* Whether to automatically reconnect when the connection ends or fails with a retryable error
|
|
311
|
+
* (network error or a `5xx` status). Terminal `4xx` responses (including auth failures) are not
|
|
312
|
+
* retried. On reconnect the last received event id is sent as `Last-Event-ID`, and the request
|
|
313
|
+
* headers (including `Authorization`) and credentials are re-applied. Defaults to `true`.
|
|
314
|
+
*/
|
|
315
|
+
reconnect?: boolean;
|
|
316
|
+
/**
|
|
317
|
+
* The delay in milliseconds before reconnecting, used until the server sends a `retry:` value.
|
|
318
|
+
* Defaults to `3000`.
|
|
319
|
+
*/
|
|
320
|
+
retry?: number;
|
|
321
|
+
};
|
|
322
|
+
|
|
253
323
|
export { }
|
|
@@ -1,17 +1,61 @@
|
|
|
1
|
-
import { IO as e,
|
|
2
|
-
//#region src/
|
|
3
|
-
var
|
|
1
|
+
import { IO as e, List as t, None as n, Option as r, Some as i, Stream as a } from "@openstage/monadyssey-core";
|
|
2
|
+
//#region src/sse.ts
|
|
3
|
+
var o = (e) => {
|
|
4
|
+
let t, n, r, i = [];
|
|
5
|
+
for (let a of e.split("\n")) {
|
|
6
|
+
if (a === "" || a.startsWith(":")) continue;
|
|
7
|
+
let e = a.indexOf(":"), o = e === -1 ? a : a.slice(0, e), s = e === -1 ? "" : a.slice(e + 1);
|
|
8
|
+
switch (s.startsWith(" ") && (s = s.slice(1)), o) {
|
|
9
|
+
case "event":
|
|
10
|
+
t = s;
|
|
11
|
+
break;
|
|
12
|
+
case "data":
|
|
13
|
+
i.push(s);
|
|
14
|
+
break;
|
|
15
|
+
case "id":
|
|
16
|
+
n = s;
|
|
17
|
+
break;
|
|
18
|
+
case "retry": {
|
|
19
|
+
let e = Number.parseInt(s, 10);
|
|
20
|
+
Number.isNaN(e) || (r = e);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
default: break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return i.length === 0 ? null : {
|
|
27
|
+
event: t ?? "message",
|
|
28
|
+
data: i.join("\n"),
|
|
29
|
+
...n === void 0 ? {} : { id: n },
|
|
30
|
+
...r === void 0 ? {} : { retry: r }
|
|
31
|
+
};
|
|
32
|
+
}, s = class {
|
|
33
|
+
decoder = new TextDecoder("utf-8");
|
|
34
|
+
buffered = "";
|
|
35
|
+
push(e) {
|
|
36
|
+
let t = this.buffered + this.decoder.decode(e, { stream: !0 }), n = !1;
|
|
37
|
+
t.endsWith("\r") && (t = t.slice(0, -1), n = !0), t = t.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
38
|
+
let r = t.split("\n\n"), i = r.pop() ?? "";
|
|
39
|
+
return n && (i += "\r"), this.buffered = i, r.map(o).filter((e) => e !== null);
|
|
40
|
+
}
|
|
41
|
+
reset() {
|
|
42
|
+
this.decoder = new TextDecoder("utf-8"), this.buffered = "";
|
|
43
|
+
}
|
|
44
|
+
}, c = () => {
|
|
45
|
+
let e = new s();
|
|
46
|
+
return (n) => n.mapAccumulate(null, (n) => [null, t.from(e.push(n))]);
|
|
47
|
+
}, l = (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), u = (e) => {
|
|
4
48
|
let t = {};
|
|
5
49
|
for (let [n, r] of Object.entries(e)) r !== void 0 && (t[n.toLowerCase()] = r);
|
|
6
50
|
return t;
|
|
7
|
-
},
|
|
51
|
+
}, d = (e, t, n) => {
|
|
8
52
|
let r = n;
|
|
9
53
|
for (let t of [...e].reverse()) {
|
|
10
54
|
let e = r;
|
|
11
55
|
r = (n) => t.intercept(n, e);
|
|
12
56
|
}
|
|
13
57
|
return r(t);
|
|
14
|
-
},
|
|
58
|
+
}, f = async (e, t, n) => {
|
|
15
59
|
if (e.status === 204 || e.status === 205) return null;
|
|
16
60
|
try {
|
|
17
61
|
switch (t) {
|
|
@@ -20,22 +64,43 @@ var r = (e) => typeof e != "object" || !e || e instanceof FormData || e instance
|
|
|
20
64
|
case "blob": return await e.blob();
|
|
21
65
|
case "arrayBuffer": return await e.arrayBuffer();
|
|
22
66
|
case "formData": return await e.formData();
|
|
23
|
-
default: throw
|
|
67
|
+
default: throw p(`Unsupported response type: ${t}`, n, e);
|
|
24
68
|
}
|
|
25
69
|
} catch (t) {
|
|
26
|
-
throw t instanceof
|
|
70
|
+
throw t instanceof y ? t : p(t, n, e);
|
|
27
71
|
}
|
|
28
|
-
},
|
|
29
|
-
let r =
|
|
30
|
-
return new
|
|
31
|
-
},
|
|
72
|
+
}, p = (e, t, n) => {
|
|
73
|
+
let r = m(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.";
|
|
74
|
+
return new y(n?.status || 500, a, i, t, r);
|
|
75
|
+
}, m = (e) => {
|
|
32
76
|
if (!e) return {};
|
|
33
77
|
try {
|
|
34
78
|
return Object.fromEntries(e.headers.entries());
|
|
35
79
|
} catch {
|
|
36
80
|
return {};
|
|
37
81
|
}
|
|
38
|
-
},
|
|
82
|
+
}, h = (e) => e.status >= 500, g = (t) => e.cancellable((e) => new Promise((n) => {
|
|
83
|
+
let r = setTimeout(n, t), i = () => {
|
|
84
|
+
clearTimeout(r), n();
|
|
85
|
+
};
|
|
86
|
+
e.aborted ? i() : e.addEventListener("abort", i, { once: !0 });
|
|
87
|
+
})), _ = class {
|
|
88
|
+
connection = null;
|
|
89
|
+
frames = new s();
|
|
90
|
+
pending = [];
|
|
91
|
+
lastEventId = void 0;
|
|
92
|
+
retryMs;
|
|
93
|
+
shouldDelay = !1;
|
|
94
|
+
constructor(e) {
|
|
95
|
+
this.retryMs = e;
|
|
96
|
+
}
|
|
97
|
+
closeConnection() {
|
|
98
|
+
this.connection &&= (this.connection.controller.abort(), this.connection.reader.cancel().catch(() => {}), null);
|
|
99
|
+
}
|
|
100
|
+
close() {
|
|
101
|
+
this.closeConnection();
|
|
102
|
+
}
|
|
103
|
+
}, v = class {
|
|
39
104
|
interceptors;
|
|
40
105
|
baseUrl;
|
|
41
106
|
defaultHeaders;
|
|
@@ -71,6 +136,46 @@ var r = (e) => typeof e != "object" || !e || e instanceof FormData || e instance
|
|
|
71
136
|
fetch(e, t, n = {}) {
|
|
72
137
|
return this.request(e, t, n);
|
|
73
138
|
}
|
|
139
|
+
sse(t, i = {}) {
|
|
140
|
+
let o = i.reconnect ?? !0, s = i.retry ?? 3e3, c = this.resolveUrl(t), l = i.headers ?? {}, v = i.credentials ?? this.defaultCredentials, b = this.interceptors, x = this.defaultHeaders, S = (t) => e.lift(async (e) => {
|
|
141
|
+
let n = new AbortController();
|
|
142
|
+
e && (e.aborted ? n.abort(e.reason) : e.addEventListener("abort", () => n.abort(e.reason), { once: !0 }));
|
|
143
|
+
let r = await d(b, {
|
|
144
|
+
method: "GET",
|
|
145
|
+
headers: u({
|
|
146
|
+
...x,
|
|
147
|
+
...l,
|
|
148
|
+
accept: "text/event-stream",
|
|
149
|
+
...t === void 0 ? {} : { "last-event-id": t }
|
|
150
|
+
}),
|
|
151
|
+
credentials: v,
|
|
152
|
+
signal: n.signal
|
|
153
|
+
}, (e) => fetch(c, e));
|
|
154
|
+
if (!r.ok) {
|
|
155
|
+
let e = await f(r, "text", c).catch(() => null);
|
|
156
|
+
throw new y(r.status, r.statusText, e, c, m(r));
|
|
157
|
+
}
|
|
158
|
+
if (!r.body) throw new y(500, "Response has no body", null, c, m(r));
|
|
159
|
+
return {
|
|
160
|
+
reader: r.body.getReader(),
|
|
161
|
+
controller: n
|
|
162
|
+
};
|
|
163
|
+
}, (e) => e instanceof y ? e : p(e, c)), C = (t) => e.cancellable((e) => {
|
|
164
|
+
let n = () => t.cancel().catch(() => {});
|
|
165
|
+
return e.aborted ? n() : e.addEventListener("abort", n, { once: !0 }), t.read();
|
|
166
|
+
}, (e) => e instanceof y ? e : p(e, c)), w = (t) => {
|
|
167
|
+
if (t.pending.length > 0) {
|
|
168
|
+
let n = t.pending.shift();
|
|
169
|
+
return n.id !== void 0 && (t.lastEventId = n.id), n.retry !== void 0 && (t.retryMs = n.retry), e.pure(r.pure([n, t]));
|
|
170
|
+
}
|
|
171
|
+
if (t.connection === null) {
|
|
172
|
+
let n = S(t.lastEventId).foldM((n) => o && h(n) ? (t.shouldDelay = !0, w(t)) : e.fail(n), (e) => (t.connection = e, t.frames.reset(), t.shouldDelay = !1, w(t)));
|
|
173
|
+
return t.shouldDelay ? g(t.retryMs).flatMap(() => n) : n;
|
|
174
|
+
}
|
|
175
|
+
return C(t.connection.reader).foldM((n) => (t.closeConnection(), o && h(n) ? (t.shouldDelay = !0, w(t)) : e.fail(n)), (r) => r.done ? (t.closeConnection(), o ? (t.shouldDelay = !0, w(t)) : e.pure(n.Instance)) : (r.value !== void 0 && t.pending.push(...t.frames.push(r.value)), w(t)));
|
|
176
|
+
};
|
|
177
|
+
return a.bracket(e.lift(() => new _(s)), (e) => a.unfoldEval(e, w), (t) => e.lift(() => t.close()));
|
|
178
|
+
}
|
|
74
179
|
resolveUrl(e) {
|
|
75
180
|
if (!this.baseUrl) return e;
|
|
76
181
|
try {
|
|
@@ -79,34 +184,34 @@ var r = (e) => typeof e != "object" || !e || e instanceof FormData || e instance
|
|
|
79
184
|
return `${this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl}${e.startsWith("/") ? e : `/${e}`}`;
|
|
80
185
|
}
|
|
81
186
|
}
|
|
82
|
-
request(t, n,
|
|
83
|
-
let
|
|
187
|
+
request(t, n, r = {}) {
|
|
188
|
+
let i = this.resolveUrl(t), a = r.timeout ?? this.defaultTimeout;
|
|
84
189
|
return e.cancellable(async (e) => {
|
|
85
|
-
let { headers: t = {}, body:
|
|
190
|
+
let { headers: t = {}, body: o, responseType: s = "json", credentials: c = this.defaultCredentials, observe: p = "body", transform: h = (e) => e } = r, g = u({
|
|
86
191
|
...this.defaultHeaders,
|
|
87
192
|
...t
|
|
88
193
|
});
|
|
89
|
-
typeof
|
|
90
|
-
let
|
|
194
|
+
typeof o == "object" && o && !l(o) && !("content-type" in g) && (g["content-type"] = "application/json"), o instanceof FormData && delete g["content-type"];
|
|
195
|
+
let _ = n === "GET" || n === "HEAD" || o == null ? void 0 : l(o) ? o : g["content-type"] === "application/json" ? JSON.stringify(o) : o, v, b, x = a == null ? e : (v = new AbortController(), e.addEventListener("abort", () => v.abort(e.reason), { once: !0 }), b = setTimeout(() => v.abort(new DOMException("Request timed out", "TimeoutError")), a), v.signal);
|
|
91
196
|
try {
|
|
92
197
|
let e = {
|
|
93
198
|
method: n,
|
|
94
|
-
headers:
|
|
95
|
-
credentials:
|
|
96
|
-
body:
|
|
199
|
+
headers: g,
|
|
200
|
+
credentials: c,
|
|
201
|
+
body: _,
|
|
97
202
|
signal: x
|
|
98
|
-
}, t = await
|
|
203
|
+
}, t = await d(this.interceptors, e, (e) => fetch(i, e));
|
|
99
204
|
if (!t.ok) {
|
|
100
|
-
let e = await
|
|
101
|
-
throw new
|
|
205
|
+
let e = await f(t, s, i), n = m(t);
|
|
206
|
+
throw new y(t.status, t.statusText, e, i, n);
|
|
102
207
|
}
|
|
103
|
-
return
|
|
208
|
+
return p === "response" ? t : h(await f(t, s, i));
|
|
104
209
|
} finally {
|
|
105
210
|
b != null && clearTimeout(b);
|
|
106
211
|
}
|
|
107
|
-
}, (e) => e instanceof
|
|
212
|
+
}, (e) => e instanceof y ? e : p(e, i));
|
|
108
213
|
}
|
|
109
|
-
},
|
|
214
|
+
}, y = class extends Error {
|
|
110
215
|
status;
|
|
111
216
|
rawMessage;
|
|
112
217
|
body;
|
|
@@ -116,10 +221,10 @@ var r = (e) => typeof e != "object" || !e || e instanceof FormData || e instance
|
|
|
116
221
|
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;
|
|
117
222
|
}
|
|
118
223
|
bodyAs(e) {
|
|
119
|
-
return e(this.body) ?
|
|
224
|
+
return e(this.body) ? i.pure(this.body) : n.Instance;
|
|
120
225
|
}
|
|
121
226
|
};
|
|
122
227
|
//#endregion
|
|
123
|
-
export {
|
|
228
|
+
export { v as HttpClient, y as HttpError, c as decodeEvents };
|
|
124
229
|
|
|
125
230
|
//# sourceMappingURL=monadyssey-fetch.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.mjs","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { IO, Option, Some, None } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType } from \"./options\";\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/**\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 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\n }\n}\n"],"mappings":"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"}
|
|
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, None, 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.pure(Option.pure<[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.pure(None.Instance as EventStep);\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, 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +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=>
|
|
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`:{let e=Number.parseInt(s,10);Number.isNaN(e)||(r=e);break}default:break}}return i.length===0?null:{event:t??`message`,data:i.join(`
|
|
3
|
+
`),...n===void 0?{}:{id:n},...r===void 0?{}:{retry:r}}},r=class{decoder=new TextDecoder(`utf-8`);buffered=``;push(e){let t=this.buffered+this.decoder.decode(e,{stream:!0}),r=!1;t.endsWith(`\r`)&&(t=t.slice(0,-1),r=!0),t=t.replace(/\r\n/g,`
|
|
4
|
+
`).replace(/\r/g,`
|
|
5
|
+
`);let i=t.split(`
|
|
6
|
+
|
|
7
|
+
`),a=i.pop()??``;return r&&(a+=`\r`),this.buffered=a,i.map(n).filter(e=>e!==null)}reset(){this.decoder=new TextDecoder(`utf-8`),this.buffered=``}},i=()=>{let e=new r;return n=>n.mapAccumulate(null,n=>[null,t.List.from(e.push(n))])},a=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),o=e=>{let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n.toLowerCase()]=r);return t},s=(e,t,n)=>{let r=n;for(let t of[...e].reverse()){let e=r;r=n=>t.intercept(n,e)}return r(t)},c=async(e,t,n)=>{if(e.status===204||e.status===205)return null;try{switch(t){case`json`:return await e.json();case`text`:return await e.text();case`blob`:return await e.blob();case`arrayBuffer`:return await e.arrayBuffer();case`formData`:return await e.formData();default:throw l(`Unsupported response type: ${t}`,n,e)}}catch(t){throw t instanceof h?t:l(t,n,e)}},l=(e,t,n)=>{let r=u(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.`;return new h(n?.status||500,a,i,t,r)},u=e=>{if(!e)return{};try{return Object.fromEntries(e.headers.entries())}catch{return{}}},d=e=>e.status>=500,f=e=>t.IO.cancellable(t=>new Promise(n=>{let r=setTimeout(n,e),i=()=>{clearTimeout(r),n()};t.aborted?i():t.addEventListener(`abort`,i,{once:!0})})),p=class{connection=null;frames=new r;pending=[];lastEventId=void 0;retryMs;shouldDelay=!1;constructor(e){this.retryMs=e}closeConnection(){this.connection&&=(this.connection.controller.abort(),this.connection.reader.cancel().catch(()=>{}),null)}close(){this.closeConnection()}},m=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,n={}){let r=n.reconnect??!0,i=n.retry??3e3,a=this.resolveUrl(e),m=n.headers??{},g=n.credentials??this.defaultCredentials,_=this.interceptors,v=this.defaultHeaders,y=e=>t.IO.lift(async t=>{let n=new AbortController;t&&(t.aborted?n.abort(t.reason):t.addEventListener(`abort`,()=>n.abort(t.reason),{once:!0}));let r=await s(_,{method:`GET`,headers:o({...v,...m,accept:`text/event-stream`,...e===void 0?{}:{"last-event-id":e}}),credentials:g,signal:n.signal},e=>fetch(a,e));if(!r.ok){let e=await c(r,`text`,a).catch(()=>null);throw new h(r.status,r.statusText,e,a,u(r))}if(!r.body)throw new h(500,`Response has no body`,null,a,u(r));return{reader:r.body.getReader(),controller:n}},e=>e instanceof h?e:l(e,a)),b=e=>t.IO.cancellable(t=>{let n=()=>e.cancel().catch(()=>{});return t.aborted?n():t.addEventListener(`abort`,n,{once:!0}),e.read()},e=>e instanceof h?e:l(e,a)),x=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=n.retry),t.IO.pure(t.Option.pure([n,e]))}if(e.connection===null){let n=y(e.lastEventId).foldM(n=>r&&d(n)?(e.shouldDelay=!0,x(e)):t.IO.fail(n),t=>(e.connection=t,e.frames.reset(),e.shouldDelay=!1,x(e)));return e.shouldDelay?f(e.retryMs).flatMap(()=>n):n}return b(e.connection.reader).foldM(n=>(e.closeConnection(),r&&d(n)?(e.shouldDelay=!0,x(e)):t.IO.fail(n)),n=>n.done?(e.closeConnection(),r?(e.shouldDelay=!0,x(e)):t.IO.pure(t.None.Instance)):(n.value!==void 0&&e.pending.push(...e.frames.push(n.value)),x(e)))};return t.Stream.bracket(t.IO.lift(()=>new p(i)),e=>t.Stream.unfoldEval(e,x),e=>t.IO.lift(()=>e.close()))}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,r={}){let i=this.resolveUrl(e),d=r.timeout??this.defaultTimeout;return t.IO.cancellable(async e=>{let{headers:t={},body:l,responseType:f=`json`,credentials:p=this.defaultCredentials,observe:m=`body`,transform:g=e=>e}=r,_=o({...this.defaultHeaders,...t});typeof l==`object`&&l&&!a(l)&&!(`content-type`in _)&&(_[`content-type`]=`application/json`),l instanceof FormData&&delete _[`content-type`];let v=n===`GET`||n===`HEAD`||l==null?void 0:a(l)?l:_[`content-type`]===`application/json`?JSON.stringify(l):l,y,b,x=d==null?e:(y=new AbortController,e.addEventListener(`abort`,()=>y.abort(e.reason),{once:!0}),b=setTimeout(()=>y.abort(new DOMException(`Request timed out`,`TimeoutError`)),d),y.signal);try{let e={method:n,headers:_,credentials:p,body:v,signal:x},t=await s(this.interceptors,e,e=>fetch(i,e));if(!t.ok){let e=await c(t,f,i),n=u(t);throw new h(t.status,t.statusText,e,i,n)}return m===`response`?t:g(await c(t,f,i))}finally{b!=null&&clearTimeout(b)}},e=>e instanceof h?e:l(e,i))}},h=class extends Error{status;rawMessage;body;url;headers;constructor(e,t,n,r,i){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}bodyAs(e){return e(this.body)?t.Some.pure(this.body):t.None.Instance}};e.HttpClient=m,e.HttpError=h,e.decodeEvents=i});
|
|
2
8
|
//# sourceMappingURL=monadyssey-fetch.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.umd.js","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { IO, Option, Some, None } from \"@openstage/monadyssey-core\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, ResponseType } from \"./options\";\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/**\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 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\n }\n}\n"],"mappings":"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"}
|
|
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, None, 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.pure(Option.pure<[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.pure(None.Instance as EventStep);\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, 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.pure(this.body as NonNullable<T>) as Option<T>) : (None.Instance as Option<T>);\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.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"prettier:fix": "npm run prettier -- --write"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@openstage/monadyssey-core": "^3.0.0-beta.
|
|
44
|
+
"@openstage/monadyssey-core": "^3.0.0-beta.4"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/readme.md
CHANGED
|
@@ -11,7 +11,7 @@ error handling, declarative workflows, and type safety when interacting with API
|
|
|
11
11
|
|
|
12
12
|
Explore the documentation for specific features:
|
|
13
13
|
|
|
14
|
-
- [HttpClient](../../docs/monadyssey-fetch/http-client.md): Composable HTTP client with base URL, interceptors, cancellation, timeout,
|
|
14
|
+
- [HttpClient](../../docs/monadyssey-fetch/http-client.md): Composable HTTP client with base URL, interceptors, cancellation, timeout, native body type support, and server-sent events (with automatic reconnection).
|
|
15
15
|
|
|
16
16
|
### Installation
|
|
17
17
|
|