@openstage/monadyssey-fetch 3.0.0-beta.2 → 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.
@@ -1,2 +1,8 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@openstage/monadyssey-core`);var t=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),n=e=>{let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n.toLowerCase()]=r);return t},r=(e,t,n)=>{let r=n;for(let t of[...e].reverse()){let e=r;r=n=>t.intercept(n,e)}return r(t)},i=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 a(`Unsupported response type: ${t}`,n,e)}}catch(t){throw t instanceof c?t:a(t,n,e)}},a=(e,t,n)=>{let r=o(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 c(n?.status||500,a,i,t,r)},o=e=>{if(!e)return{};try{return Object.fromEntries(e.headers.entries())}catch{return{}}},s=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)}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(s,l,u={}){let d=this.resolveUrl(s),f=u.timeout??this.defaultTimeout;return e.IO.cancellable(async e=>{let{headers:a={},body:s,responseType:p=`json`,credentials:m=this.defaultCredentials,observe:h=`body`,transform:g=e=>e}=u,_=n({...this.defaultHeaders,...a});typeof s==`object`&&s&&!t(s)&&!(`content-type`in _)&&(_[`content-type`]=`application/json`),s instanceof FormData&&delete _[`content-type`];let v=l===`GET`||l===`HEAD`||s==null?void 0:t(s)?s:_[`content-type`]===`application/json`?JSON.stringify(s):s,y,b,x=f==null?e:(y=new AbortController,e.addEventListener(`abort`,()=>y.abort(e.reason),{once:!0}),b=setTimeout(()=>y.abort(new DOMException(`Request timed out`,`TimeoutError`)),f),y.signal);try{let e={method:l,headers:_,credentials:m,body:v,signal:x},t=await r(this.interceptors,e,e=>fetch(d,e));if(!t.ok){let e=await i(t,p,d),n=o(t);throw new c(t.status,t.statusText,e,d,n)}return h===`response`?t:g(await i(t,p,d))}finally{b!=null&&clearTimeout(b)}},e=>e instanceof c?e:a(e,d))}},c=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=s,exports.HttpError=c;
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, None as t, Some as n } from "@openstage/monadyssey-core";
2
- //#region src/http-client.ts
3
- var r = (e) => typeof e != "object" || !e || e instanceof FormData || e instanceof Blob || e instanceof ArrayBuffer || e instanceof URLSearchParams || typeof ReadableStream < "u" && e instanceof ReadableStream || ArrayBuffer.isView(e), i = (e) => {
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
- }, a = (e, t, n) => {
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
- }, o = async (e, t, n) => {
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 s(`Unsupported response type: ${t}`, n, e);
67
+ default: throw p(`Unsupported response type: ${t}`, n, e);
24
68
  }
25
69
  } catch (t) {
26
- throw t instanceof u ? t : s(t, n, e);
70
+ throw t instanceof y ? t : p(t, n, e);
27
71
  }
28
- }, s = (e, t, n) => {
29
- let r = c(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.";
30
- return new u(n?.status || 500, a, i, t, r);
31
- }, c = (e) => {
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
- }, l = class {
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, l = {}) {
83
- let d = this.resolveUrl(t), f = l.timeout ?? this.defaultTimeout;
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: s, responseType: p = "json", credentials: m = this.defaultCredentials, observe: h = "body", transform: g = (e) => e } = l, _ = i({
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 s == "object" && s && !r(s) && !("content-type" in _) && (_["content-type"] = "application/json"), s instanceof FormData && delete _["content-type"];
90
- let v = n === "GET" || n === "HEAD" || s == null ? void 0 : r(s) ? s : _["content-type"] === "application/json" ? JSON.stringify(s) : s, y, b, x = f == null ? e : (y = new AbortController(), e.addEventListener("abort", () => y.abort(e.reason), { once: !0 }), b = setTimeout(() => y.abort(new DOMException("Request timed out", "TimeoutError")), f), y.signal);
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: m,
96
- body: v,
199
+ headers: g,
200
+ credentials: c,
201
+ body: _,
97
202
  signal: x
98
- }, t = await a(this.interceptors, e, (e) => fetch(d, e));
203
+ }, t = await d(this.interceptors, e, (e) => fetch(i, e));
99
204
  if (!t.ok) {
100
- let e = await o(t, p, d), n = c(t);
101
- throw new u(t.status, t.statusText, e, d, n);
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 h === "response" ? t : g(await o(t, p, d));
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 u ? e : s(e, d));
212
+ }, (e) => e instanceof y ? e : p(e, i));
108
213
  }
109
- }, u = class extends Error {
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) ? n.pure(this.body) : t.Instance;
224
+ return e(this.body) ? i.pure(this.body) : n.Instance;
120
225
  }
121
226
  };
122
227
  //#endregion
123
- export { l as HttpClient, u as HttpError };
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=>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),r=e=>{let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n.toLowerCase()]=r);return t},i=(e,t,n)=>{let r=n;for(let t of[...e].reverse()){let e=r;r=n=>t.intercept(n,e)}return r(t)},a=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 o(`Unsupported response type: ${t}`,n,e)}}catch(t){throw t instanceof l?t:o(t,n,e)}},o=(e,t,n)=>{let r=s(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 l(n?.status||500,a,i,t,r)},s=e=>{if(!e)return{};try{return Object.fromEntries(e.headers.entries())}catch{return{}}},c=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)}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,c,u={}){let d=this.resolveUrl(e),f=u.timeout??this.defaultTimeout;return t.IO.cancellable(async e=>{let{headers:t={},body:o,responseType:p=`json`,credentials:m=this.defaultCredentials,observe:h=`body`,transform:g=e=>e}=u,_=r({...this.defaultHeaders,...t});typeof o==`object`&&o&&!n(o)&&!(`content-type`in _)&&(_[`content-type`]=`application/json`),o instanceof FormData&&delete _[`content-type`];let v=c===`GET`||c===`HEAD`||o==null?void 0:n(o)?o:_[`content-type`]===`application/json`?JSON.stringify(o):o,y,b,x=f==null?e:(y=new AbortController,e.addEventListener(`abort`,()=>y.abort(e.reason),{once:!0}),b=setTimeout(()=>y.abort(new DOMException(`Request timed out`,`TimeoutError`)),f),y.signal);try{let e={method:c,headers:_,credentials:m,body:v,signal:x},t=await i(this.interceptors,e,e=>fetch(d,e));if(!t.ok){let e=await a(t,p,d),n=s(t);throw new l(t.status,t.statusText,e,d,n)}return h===`response`?t:g(await a(t,p,d))}finally{b!=null&&clearTimeout(b)}},e=>e instanceof l?e:o(e,d))}},l=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=c,e.HttpError=l});
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.2",
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.2"
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, and native body type support.
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
 
@@ -100,11 +100,11 @@ await api.fetch<{ message: string }>("/custom", "OPTIONS", {
100
100
  | **Option** | **Type** | **Description** |
101
101
  |----------------|-------------------------------------------------------------|-------------------------------------------------------------------------------|
102
102
  | `headers` | `Record<string, string>` | Custom headers. Merged with defaults, keys normalized to lowercase |
103
- | `body` | `any` | Request payload. Objects auto-JSON; FormData/Blob passed as-is |
103
+ | `body` | `unknown` | Request payload. Objects auto-JSON; FormData/Blob passed as-is |
104
104
  | `responseType`| `"json"`, `"text"`, `"blob"`, `"arrayBuffer"`, `"formData"` | Expected response type. **Defaults to `"json"`** |
105
105
  | `credentials` | `"omit"`, `"same-origin"`, `"include"` | Credential policy. **Defaults to client setting (`"same-origin"`)** |
106
106
  | `observe` | `"body"` or `"response"` | Return parsed body or full `Response`. **Defaults to `"body"`** |
107
- | `transform` | `(data: any) => A` | Transform the response data |
107
+ | `transform` | `(data: unknown) => A` | Transform the response data |
108
108
  | `timeout` | `number` | Per-request timeout in milliseconds. Overrides client-level timeout |
109
109
 
110
110
  ### Error Handling
@@ -114,7 +114,7 @@ Errors are encapsulated in the `HttpError` type, which includes:
114
114
  * `status`: The HTTP status code (500 for network errors)
115
115
  * `message`: Formatted error message containing the URL, status, and raw message
116
116
  * `rawMessage`: The raw error description
117
- * `body`: The response body if available
117
+ * `body`: The response body if available, typed `unknown`. Narrow it safely with `error.bodyAs(guard)`, which returns `Option<T>`
118
118
  * `url`: The request URL
119
119
  * `headers`: The response headers if available
120
120