@openstage/monadyssey-fetch 3.0.0-rc.1 → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/monadyssey-fetch.d.ts +46 -11
- package/dist/monadyssey-fetch.mjs +125 -98
- package/dist/monadyssey-fetch.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -21,6 +21,16 @@ export declare type Credentials = "omit" | "same-origin" | "include";
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const decodeEvents: <E>() => Pipe<E, Uint8Array, ServerSentEvent>;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `T` restricted to the keys of `Shape`: every other key is typed `never`, so an object with an extra
|
|
26
|
+
* key is rejected even when it is a variable rather than a literal. `send` / `raw` take their options
|
|
27
|
+
* this way, so a payload passed positionally (`send("POST", url, article)`, where `article` happens to
|
|
28
|
+
* have a `body` key) does not type-check as the options.
|
|
29
|
+
*/
|
|
30
|
+
declare type Exact<T, Shape> = T & {
|
|
31
|
+
[K in Exclude<keyof T, keyof Shape>]: never;
|
|
32
|
+
};
|
|
33
|
+
|
|
24
34
|
/**
|
|
25
35
|
* A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of
|
|
26
36
|
* Promises. Each instance carries its own configuration (base URL, interceptors, default headers,
|
|
@@ -143,8 +153,10 @@ export declare class HttpClient {
|
|
|
143
153
|
*
|
|
144
154
|
* @param {Method} method - The HTTP method.
|
|
145
155
|
* @param {string} uri - The URL or path to request.
|
|
146
|
-
* @param {SendOptions} [options] - Headers, credentials, timeout and body.
|
|
147
|
-
*
|
|
156
|
+
* @param {SendOptions} [options] - Headers, credentials, timeout and body. Every key must be one of
|
|
157
|
+
* `SendOptions`: a payload passed here by mistake (`send("POST", url, article)`) is rejected at
|
|
158
|
+
* compile time; it belongs in `{ body: article }`. A `GET` or `HEAD` request with a body throws
|
|
159
|
+
* `TypeError` where it is built.
|
|
148
160
|
* @returns {IO<HttpError, void>}
|
|
149
161
|
*
|
|
150
162
|
* @example
|
|
@@ -152,7 +164,7 @@ export declare class HttpClient {
|
|
|
152
164
|
* client.send("PUT", `/api/v1/articles/${id}`, { body: article });
|
|
153
165
|
* client.send("DELETE", `/api/v1/passkeys/${id}`, { headers: { "X-Step-Up": token } });
|
|
154
166
|
*/
|
|
155
|
-
send(method: Method, uri: string, options?: SendOptions): IO<HttpError, void>;
|
|
167
|
+
send<O extends SendOptions>(method: Method, uri: string, options?: Exact<O, SendOptions>): IO<HttpError, void>;
|
|
156
168
|
/**
|
|
157
169
|
* Performs a request and yields the `Response` itself, for status codes, headers or streaming.
|
|
158
170
|
* A non-2xx response still fails with `kind: "response"` (its body read into the error); a 2xx
|
|
@@ -161,13 +173,14 @@ export declare class HttpClient {
|
|
|
161
173
|
*
|
|
162
174
|
* @param {Method} method - The HTTP method.
|
|
163
175
|
* @param {string} uri - The URL or path to request.
|
|
164
|
-
* @param {SendOptions} [options] - Headers, credentials, timeout and body
|
|
176
|
+
* @param {SendOptions} [options] - Headers, credentials, timeout and body; every key must be one of
|
|
177
|
+
* `SendOptions`, as for `send`.
|
|
165
178
|
* @returns {IO<HttpError, Response>}
|
|
166
179
|
*
|
|
167
180
|
* @example
|
|
168
181
|
* client.raw("POST", "/api/v1/communities", { body: input }).map((r) => r.headers.get("Location"));
|
|
169
182
|
*/
|
|
170
|
-
raw(method: Method, uri: string, options?: SendOptions): IO<HttpError, Response>;
|
|
183
|
+
raw<O extends SendOptions>(method: Method, uri: string, options?: Exact<O, SendOptions>): IO<HttpError, Response>;
|
|
171
184
|
/**
|
|
172
185
|
* Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.
|
|
173
186
|
*
|
|
@@ -202,13 +215,18 @@ export declare class HttpClient {
|
|
|
202
215
|
* Configuration for creating an HttpClient instance.
|
|
203
216
|
*/
|
|
204
217
|
export declare type HttpClientConfig = {
|
|
205
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Prepended to relative paths by string concatenation with exactly one `/` between them, so the
|
|
220
|
+
* base's own path is kept: `baseUrl: "https://h/api"` and `"/v1/x"` give `https://h/api/v1/x`
|
|
221
|
+
* (`new URL("/v1/x", base)` would drop `/api`). A `uri` with a scheme (`https://…`, `data:`,
|
|
222
|
+
* `blob:`) is used as is.
|
|
223
|
+
*/
|
|
206
224
|
baseUrl?: string;
|
|
207
225
|
/** Interceptors applied in registration order (first registered = outermost). Immutable after construction. */
|
|
208
226
|
interceptors?: HttpInterceptor[];
|
|
209
227
|
/** Default headers merged into every request. Per-request headers take precedence. */
|
|
210
228
|
defaultHeaders?: Record<string, string>;
|
|
211
|
-
/** Default timeout in milliseconds for all requests. Can be overridden per-request. */
|
|
229
|
+
/** Default timeout in milliseconds for all requests; none by default. Can be overridden per-request. */
|
|
212
230
|
timeout?: number;
|
|
213
231
|
/** Default credentials policy. Defaults to `"same-origin"` (matches the platform `fetch` default). */
|
|
214
232
|
credentials?: Credentials;
|
|
@@ -226,6 +244,10 @@ export declare type HttpClientConfig = {
|
|
|
226
244
|
* For every kind other than `"response"` and `"decode"`, `status` is `0`; `body` is `null` except
|
|
227
245
|
* for `"response"`.
|
|
228
246
|
*
|
|
247
|
+
* `body`, `headers` and `url` hold what the server and the request contained, which may be sensitive
|
|
248
|
+
* (a token in a query string, an error page echoing input); redact them before logging an error
|
|
249
|
+
* outside a trusted sink. `Set-Cookie` is never carried.
|
|
250
|
+
*
|
|
229
251
|
* @example
|
|
230
252
|
* client.post("/payments", payment).mapErr((error) =>
|
|
231
253
|
* error.kind === "response"
|
|
@@ -236,7 +258,7 @@ export declare type HttpClientConfig = {
|
|
|
236
258
|
export declare class HttpError extends Error {
|
|
237
259
|
/** The transport outcome. See {@link HttpErrorKind}. */
|
|
238
260
|
readonly kind: HttpErrorKind;
|
|
239
|
-
/** The HTTP status code, or `0` when no response arrived
|
|
261
|
+
/** The HTTP status code (`"response"` and `"decode"`), or `0` when no response arrived. */
|
|
240
262
|
readonly status: number;
|
|
241
263
|
readonly rawMessage: string;
|
|
242
264
|
/** The parsed response body, or `null` when none was parsed. */
|
|
@@ -344,6 +366,10 @@ export declare interface HttpInterceptor {
|
|
|
344
366
|
* `next({ url: request.url, init: { ...request.init, headers } })`. Return a
|
|
345
367
|
* `Promise<Response>` without calling `next` to short-circuit the request.
|
|
346
368
|
*
|
|
369
|
+
* Keep `request.init.signal` when building the request you pass on: it is how a timeout or a
|
|
370
|
+
* cancelled fiber aborts the `fetch`. A request forwarded without it runs to completion in the
|
|
371
|
+
* background after the IO has already failed or been cancelled.
|
|
372
|
+
*
|
|
347
373
|
* @param request - The resolved URL and `fetch` options of the pending request.
|
|
348
374
|
* @param next - Forwards the request to the next interceptor or to `fetch`.
|
|
349
375
|
* @returns A Promise of the resulting `Response`.
|
|
@@ -364,7 +390,8 @@ export declare type InterceptedRequest = {
|
|
|
364
390
|
};
|
|
365
391
|
|
|
366
392
|
/**
|
|
367
|
-
* Represents the HTTP method for the request.
|
|
393
|
+
* Represents the HTTP method for the request. Methods may be added in a minor release, so give an
|
|
394
|
+
* exhaustive `switch` or a `Record<Method, …>` a fallback.
|
|
368
395
|
*/
|
|
369
396
|
export declare type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
|
|
370
397
|
|
|
@@ -390,7 +417,8 @@ export declare type Options<A = unknown> = RequestOptions & {
|
|
|
390
417
|
*/
|
|
391
418
|
transform?: (data: unknown) => A;
|
|
392
419
|
/**
|
|
393
|
-
* What a response without a body (`204`, `205`, an empty
|
|
420
|
+
* What a response without a body means (`204`, `205`, a `HEAD` response, or an empty 2xx under
|
|
421
|
+
* `responseType: "json"`; under `"text"` an empty body is `""`, not absence):
|
|
394
422
|
* - `"error"` (default): a value was expected, so it fails with `kind: "decode"`;
|
|
395
423
|
* - `"option"`: absence is expected, so the request yields `Option<A>` — `None` for no body.
|
|
396
424
|
*
|
|
@@ -407,7 +435,11 @@ export declare type RequestOptions = {
|
|
|
407
435
|
headers?: Record<string, string>;
|
|
408
436
|
/** The credential policy for the request. Defaults to the client-level setting. */
|
|
409
437
|
credentials?: Credentials;
|
|
410
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Per-request timeout in milliseconds. Overrides the client-level timeout. Values above
|
|
440
|
+
* 2,147,483,647 ms (the `setTimeout` limit), including `Infinity`, are lowered to it; a negative
|
|
441
|
+
* or `NaN` value throws `RangeError` where the request is built.
|
|
442
|
+
*/
|
|
411
443
|
timeout?: number;
|
|
412
444
|
};
|
|
413
445
|
|
|
@@ -423,6 +455,9 @@ declare type RequiredBody<A> = Options<A> & {
|
|
|
423
455
|
* - `"blob"`: Parse the response as a Blob.
|
|
424
456
|
* - `"arrayBuffer"`: Parse the response as an ArrayBuffer.
|
|
425
457
|
* - `"formData"`: Parse the response as FormData.
|
|
458
|
+
*
|
|
459
|
+
* Response types may be added in a minor release, so give an exhaustive `switch` or a
|
|
460
|
+
* `Record<ResponseType, …>` a fallback.
|
|
426
461
|
*/
|
|
427
462
|
declare type ResponseType_2 = "json" | "text" | "blob" | "arrayBuffer" | "formData";
|
|
428
463
|
export { ResponseType_2 as ResponseType }
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { IO as e, List as t, Option as n, Stream as r } from "@openstage/monadyssey-core";
|
|
2
|
-
|
|
3
|
-
var i = {
|
|
2
|
+
// src/http-error.ts
|
|
3
|
+
var i = (e) => {
|
|
4
|
+
try {
|
|
5
|
+
return Object.fromEntries(Array.from(e.headers.entries()).filter(([e]) => e !== "set-cookie" && e !== "set-cookie2"));
|
|
6
|
+
} catch {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
}, a = {
|
|
4
10
|
describe: (e) => e instanceof Error ? e.message : typeof e == "string" ? e : typeof e == "object" && e && "message" in e ? String(e.message) : "An unknown error occurred.",
|
|
5
11
|
format: (e, t, n, r, i) => {
|
|
6
12
|
switch (e) {
|
|
@@ -12,7 +18,7 @@ var i = {
|
|
|
12
18
|
default: return `Request to '${t}' failed before a response arrived: ${r}.`;
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
|
-
},
|
|
21
|
+
}, o = class e extends Error {
|
|
16
22
|
kind;
|
|
17
23
|
status;
|
|
18
24
|
rawMessage;
|
|
@@ -21,49 +27,44 @@ var i = {
|
|
|
21
27
|
headers;
|
|
22
28
|
cause;
|
|
23
29
|
timeoutMs;
|
|
24
|
-
constructor(e, t, n, r,
|
|
25
|
-
super(
|
|
30
|
+
constructor(e, t, n, r, i, o = "response", s, c) {
|
|
31
|
+
super(a.format(o, r, e, t, c)), this.name = "HttpError", this.kind = o, this.status = e, this.rawMessage = t, this.body = n, this.url = r, this.headers = i, this.cause = s, this.timeoutMs = c;
|
|
26
32
|
}
|
|
27
33
|
static network(t, n) {
|
|
28
|
-
return new e(0,
|
|
34
|
+
return new e(0, a.describe(n), null, t, void 0, "network", n);
|
|
29
35
|
}
|
|
30
36
|
static timeout(t, n) {
|
|
31
37
|
return new e(0, `Request timed out after ${n} ms`, null, t, void 0, "timeout", void 0, n);
|
|
32
38
|
}
|
|
33
39
|
static decode(t, n, r) {
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
a = Object.fromEntries(n.headers.entries());
|
|
37
|
-
} catch {}
|
|
38
|
-
return new e(n.status, i.describe(r), null, t, a, "decode", r);
|
|
40
|
+
return new e(n.status, a.describe(r), null, t, i(n), "decode", r);
|
|
39
41
|
}
|
|
40
42
|
static request(t, n) {
|
|
41
|
-
return new e(0,
|
|
43
|
+
return new e(0, a.describe(n), null, t, void 0, "request", n);
|
|
42
44
|
}
|
|
43
45
|
static aborted(t, n) {
|
|
44
|
-
return new e(0,
|
|
46
|
+
return new e(0, a.describe(n), null, t, void 0, "aborted", n);
|
|
45
47
|
}
|
|
46
48
|
bodyAs(e) {
|
|
47
49
|
return e(this.body) ? n.ofNullable(this.body) : n.empty();
|
|
48
50
|
}
|
|
49
|
-
},
|
|
51
|
+
}, s = (e) => typeof e != "object" || !e || e instanceof FormData || e instanceof Blob || e instanceof ArrayBuffer || e instanceof URLSearchParams || typeof ReadableStream < "u" && e instanceof ReadableStream || ArrayBuffer.isView(e), c = (e) => Object.fromEntries(Object.entries(e).filter((e) => e[1] !== void 0).map(([e, t]) => [e.toLowerCase(), t])), l = (e, t) => {
|
|
50
52
|
if (t instanceof FormData) {
|
|
51
53
|
let { "content-type": t, ...n } = e;
|
|
52
54
|
return n;
|
|
53
55
|
}
|
|
54
|
-
return typeof t == "object" && t && !
|
|
56
|
+
return typeof t == "object" && t && !s(t) && !("content-type" in e) ? {
|
|
55
57
|
...e,
|
|
56
58
|
"content-type": "application/json"
|
|
57
59
|
} : e;
|
|
58
|
-
},
|
|
59
|
-
if (e === void 0) return !1;
|
|
60
|
+
}, u = (e) => {
|
|
60
61
|
let t = e.split(";")[0].trim().toLowerCase();
|
|
61
62
|
return t === "application/json" || t.endsWith("+json");
|
|
62
|
-
},
|
|
63
|
-
isNativeBody:
|
|
64
|
-
normalizeHeaders:
|
|
63
|
+
}, d = (e, t, n) => e === "GET" || e === "HEAD" || t == null ? void 0 : s(t) ? t : u(n["content-type"]) ? JSON.stringify(t) : t, f = {
|
|
64
|
+
isNativeBody: s,
|
|
65
|
+
normalizeHeaders: c,
|
|
65
66
|
prepare: (e, t, n) => {
|
|
66
|
-
let r = c(
|
|
67
|
+
let r = l(c({
|
|
67
68
|
...n.headers,
|
|
68
69
|
...t.headers ?? {}
|
|
69
70
|
}), t.body);
|
|
@@ -71,33 +72,27 @@ var i = {
|
|
|
71
72
|
method: e,
|
|
72
73
|
headers: r,
|
|
73
74
|
credentials: t.credentials ?? n.credentials,
|
|
74
|
-
body:
|
|
75
|
+
body: d(e, t.body, r)
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
|
-
},
|
|
78
|
+
}, p = {
|
|
78
79
|
json: (e) => e.json(),
|
|
79
80
|
text: (e) => e.text(),
|
|
80
81
|
blob: (e) => e.blob(),
|
|
81
82
|
arrayBuffer: (e) => e.arrayBuffer(),
|
|
82
83
|
formData: (e) => e.formData()
|
|
83
|
-
},
|
|
84
|
-
try {
|
|
85
|
-
return Object.fromEntries(e.headers.entries());
|
|
86
|
-
} catch {
|
|
87
|
-
return {};
|
|
88
|
-
}
|
|
89
|
-
}, h = (e, t, n) => a.decode(t, n, e), g = Symbol("HttpResponse.noBody"), _ = (e, t, n) => new a(e.status, e.statusText, t, n, m(e)), v = async (e) => {
|
|
84
|
+
}, m = (e) => Object.prototype.hasOwnProperty.call(p, e) ? n.of(p[e]) : n.empty(), h = i, g = (e, t, n) => o.decode(t, n, e), _ = Symbol("HttpResponse.noBody"), v = (e, t, n) => new o(e.status, e.statusText, t, n, h(e)), y = async (e) => {
|
|
90
85
|
if (typeof e.text != "function") return e.json();
|
|
91
86
|
let t = await e.text();
|
|
92
|
-
return t.trim() === "" ?
|
|
93
|
-
},
|
|
94
|
-
headers:
|
|
95
|
-
error:
|
|
96
|
-
failure:
|
|
97
|
-
read: (t, n, r, i) => e.from(
|
|
87
|
+
return t.trim() === "" ? _ : JSON.parse(t);
|
|
88
|
+
}, b = {
|
|
89
|
+
headers: h,
|
|
90
|
+
error: g,
|
|
91
|
+
failure: v,
|
|
92
|
+
read: (t, n, r, i) => e.from(m(n), () => g(`Unsupported response type: ${n}`, r, t)).flatMap((a) => e.lift(async () => t.status === 204 || t.status === 205 || i === "HEAD" ? _ : n === "json" ? y(t) : a(t), (e) => g(e, r, t))),
|
|
98
93
|
readFailure: (t, n, r) => e.promise(async () => {
|
|
99
94
|
try {
|
|
100
|
-
if (n !== "json") return await
|
|
95
|
+
if (n !== "json") return await m(n).fold(async () => null, (e) => e(t));
|
|
101
96
|
if (typeof t.text != "function") return await t.json();
|
|
102
97
|
let e = await t.text();
|
|
103
98
|
if (e.trim() === "") return null;
|
|
@@ -109,21 +104,29 @@ var i = {
|
|
|
109
104
|
} catch {
|
|
110
105
|
return null;
|
|
111
106
|
}
|
|
112
|
-
}).flatMap((n) => e.fail(
|
|
113
|
-
},
|
|
114
|
-
error: (e, t) => e instanceof
|
|
107
|
+
}).flatMap((n) => e.fail(v(t, n, r)))
|
|
108
|
+
}, x = (e) => typeof e == "object" && !!e && "name" in e && e.name === "AbortError", S = {
|
|
109
|
+
error: (e, t) => e instanceof o ? e : x(e) ? o.aborted(t, e) : o.network(t, e),
|
|
115
110
|
isRetryable: (e) => e.kind === "network" || e.kind === "response" && e.status >= 500
|
|
116
|
-
},
|
|
111
|
+
}, C = {
|
|
117
112
|
run: (e, t, n) => e.reduceRight((e, t) => (n) => t.intercept(n, e), n)(t),
|
|
118
|
-
request: (e, t) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
request: (e, t) => {
|
|
114
|
+
let n;
|
|
115
|
+
try {
|
|
116
|
+
n = new Headers(t.headers);
|
|
117
|
+
} catch (t) {
|
|
118
|
+
throw o.request(e, t);
|
|
123
119
|
}
|
|
124
|
-
|
|
120
|
+
return {
|
|
121
|
+
url: e,
|
|
122
|
+
init: {
|
|
123
|
+
...t,
|
|
124
|
+
headers: n
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
},
|
|
125
128
|
fetch: (e) => fetch(e.url, e.init)
|
|
126
|
-
},
|
|
129
|
+
}, w = (e) => {
|
|
127
130
|
let t = "", n, r, i = [];
|
|
128
131
|
for (let a of e.split("\n")) {
|
|
129
132
|
if (a === "" || a.startsWith(":")) continue;
|
|
@@ -151,48 +154,58 @@ var i = {
|
|
|
151
154
|
...n === void 0 ? {} : { id: n },
|
|
152
155
|
...r === void 0 ? {} : { retry: r }
|
|
153
156
|
};
|
|
154
|
-
},
|
|
157
|
+
}, T = (e, t) => {
|
|
155
158
|
let { id: n, ...r } = e;
|
|
156
159
|
return t === void 0 ? r : {
|
|
157
160
|
...r,
|
|
158
161
|
id: t
|
|
159
162
|
};
|
|
160
|
-
},
|
|
163
|
+
}, E = class {
|
|
164
|
+
limit;
|
|
161
165
|
decoder = new TextDecoder("utf-8");
|
|
162
166
|
buffered = "";
|
|
167
|
+
constructor(e = Infinity) {
|
|
168
|
+
this.limit = e;
|
|
169
|
+
}
|
|
163
170
|
skipLeadingLF = !1;
|
|
164
171
|
push(e) {
|
|
165
172
|
let t = this.decoder.decode(e, { stream: !0 });
|
|
166
173
|
if (t.length === 0) return [];
|
|
167
|
-
this.skipLeadingLF && t.startsWith("\n") && (t = t.slice(1)), this.skipLeadingLF = t.endsWith("\r")
|
|
168
|
-
let n =
|
|
169
|
-
|
|
174
|
+
this.skipLeadingLF && t.startsWith("\n") && (t = t.slice(1)), this.skipLeadingLF = t.endsWith("\r");
|
|
175
|
+
let n = Math.max(0, this.buffered.length - 1);
|
|
176
|
+
if (t = this.buffered + t.replace(/\r\n/g, "\n").replace(/\r/g, "\n"), t.indexOf("\n\n", n) === -1) return this.buffered = t, this.checkLimit(), [];
|
|
177
|
+
let r = t.split("\n\n");
|
|
178
|
+
return this.buffered = r.pop() ?? "", this.checkLimit(), r.map(w);
|
|
170
179
|
}
|
|
171
180
|
reset() {
|
|
172
181
|
this.decoder = new TextDecoder("utf-8"), this.buffered = "", this.skipLeadingLF = !1;
|
|
173
182
|
}
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
checkLimit() {
|
|
184
|
+
if (this.buffered.length > this.limit) throw RangeError(`An event block exceeds the ${this.limit}-character limit without ending`);
|
|
185
|
+
}
|
|
186
|
+
}, D = () => {
|
|
187
|
+
let e = new E();
|
|
176
188
|
return (n) => n.mapAccumulate(void 0, (n, r) => {
|
|
177
189
|
let i = [], a = n;
|
|
178
|
-
for (let t of e.push(r)) t.id !== void 0 && (a = t.id), t.event !== null && i.push(
|
|
190
|
+
for (let t of e.push(r)) t.id !== void 0 && (a = t.id), t.event !== null && i.push(T(t.event, a));
|
|
179
191
|
return [a, t.from(i)];
|
|
180
192
|
});
|
|
181
|
-
},
|
|
193
|
+
}, O = 100, k = 2147483647, A = 1048576, j = (e) => Array.from(new TextEncoder().encode(e), (e) => String.fromCharCode(e)).join(""), M = (t) => e.promise((e) => new Promise((n) => {
|
|
182
194
|
let r = () => {
|
|
183
195
|
clearTimeout(i), n();
|
|
184
196
|
}, i = setTimeout(() => {
|
|
185
197
|
e.removeEventListener("abort", r), n();
|
|
186
|
-
}, Math.min(t,
|
|
198
|
+
}, Math.min(t, k));
|
|
187
199
|
e.addEventListener("abort", r, { once: !0 });
|
|
188
|
-
})),
|
|
200
|
+
})), N = class {
|
|
189
201
|
controller = new AbortController();
|
|
190
|
-
frames = new
|
|
202
|
+
frames = new E(A);
|
|
191
203
|
pending = [];
|
|
192
204
|
lastEventId = void 0;
|
|
193
205
|
retryMs;
|
|
194
206
|
serverRetryMs = void 0;
|
|
195
207
|
reconnects = 0;
|
|
208
|
+
status = 200;
|
|
196
209
|
connection = null;
|
|
197
210
|
unlinkParent;
|
|
198
211
|
constructor(e, t) {
|
|
@@ -203,11 +216,12 @@ var i = {
|
|
|
203
216
|
get reader() {
|
|
204
217
|
return this.connection?.reader ?? null;
|
|
205
218
|
}
|
|
206
|
-
attach(e) {
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
attach(e, t) {
|
|
220
|
+
this.status = t;
|
|
221
|
+
let n = () => e.cancel().catch(() => {});
|
|
222
|
+
this.controller.signal.addEventListener("abort", n, { once: !0 }), this.connection = {
|
|
209
223
|
reader: e,
|
|
210
|
-
unlink: () => this.controller.signal.removeEventListener("abort",
|
|
224
|
+
unlink: () => this.controller.signal.removeEventListener("abort", n)
|
|
211
225
|
}, this.frames.reset();
|
|
212
226
|
}
|
|
213
227
|
closeConnection() {
|
|
@@ -216,43 +230,57 @@ var i = {
|
|
|
216
230
|
close() {
|
|
217
231
|
this.closeConnection(), this.unlinkParent(), this.controller.abort();
|
|
218
232
|
}
|
|
219
|
-
},
|
|
220
|
-
let { url: i, headers: a, defaultHeaders:
|
|
221
|
-
let n =
|
|
233
|
+
}, P = (e) => e.split(";")[0].trim().toLowerCase(), F = { stream: (t) => {
|
|
234
|
+
let { url: i, headers: a, defaultHeaders: s, credentials: c, interceptors: l, reconnect: u, retry: d } = t, p = (t) => {
|
|
235
|
+
let n = f.prepare("GET", {
|
|
222
236
|
headers: {
|
|
223
237
|
...a,
|
|
224
238
|
accept: "text/event-stream",
|
|
225
|
-
...t.lastEventId ? { "last-event-id": t.lastEventId } : {}
|
|
239
|
+
...t.lastEventId ? { "last-event-id": j(t.lastEventId) } : {}
|
|
226
240
|
},
|
|
227
|
-
credentials:
|
|
241
|
+
credentials: c
|
|
228
242
|
}, {
|
|
229
|
-
headers:
|
|
230
|
-
credentials:
|
|
243
|
+
headers: s,
|
|
244
|
+
credentials: c
|
|
231
245
|
});
|
|
232
|
-
return e.lift(() =>
|
|
246
|
+
return e.lift(() => C.run(l, C.request(i, {
|
|
233
247
|
...n,
|
|
248
|
+
cache: "no-store",
|
|
234
249
|
signal: t.controller.signal
|
|
235
|
-
}),
|
|
250
|
+
}), C.fetch), (e) => S.error(e, i)).flatMap((t) => t.ok ? e.of(t) : b.readFailure(t, "text", i)).flatMap((t) => {
|
|
236
251
|
if (t.status === 204) return t.body?.cancel().catch(() => {}), e.of(null);
|
|
237
252
|
let n = t.headers.get("content-type");
|
|
238
|
-
return n !== null &&
|
|
253
|
+
return n !== null && P(n) !== "text/event-stream" ? (t.body?.cancel().catch(() => {}), e.fail(b.error(`Expected text/event-stream, got ${n}`, i, t))) : t.body === null ? e.fail(b.error("Response has no body", i, t)) : e.of({
|
|
254
|
+
reader: t.body.getReader(),
|
|
255
|
+
status: t.status
|
|
256
|
+
});
|
|
239
257
|
});
|
|
240
|
-
},
|
|
241
|
-
if (
|
|
258
|
+
}, m = (t) => e.lift(() => t.read(), (e) => S.error(e, i)), h = e.of(n.empty()), g = (e, t) => {
|
|
259
|
+
if (u === !1) return t;
|
|
242
260
|
let n = e.reconnects + 1;
|
|
243
|
-
if (
|
|
244
|
-
let r = e.serverRetryMs ?? (
|
|
245
|
-
return e.reconnects = n,
|
|
246
|
-
},
|
|
261
|
+
if (u !== !0 && !u.allowsAttempt(n)) return t;
|
|
262
|
+
let r = e.serverRetryMs ?? (u === !0 ? e.retryMs : u.delayAfter(n));
|
|
263
|
+
return e.reconnects = n, M(r).flatMap(() => _(e));
|
|
264
|
+
}, _ = (t) => {
|
|
247
265
|
for (; t.pending.length > 0;) {
|
|
248
266
|
let r = t.pending.shift();
|
|
249
|
-
if (r.id !== void 0 && (t.lastEventId = r.id), r.retry !== void 0 && (t.serverRetryMs = Math.max(r.retry,
|
|
267
|
+
if (r.id !== void 0 && (t.lastEventId = r.id), r.retry !== void 0 && (t.serverRetryMs = Math.max(r.retry, O)), r.event !== null) return t.reconnects = 0, e.of(n.of([T(r.event, t.lastEventId), t]));
|
|
250
268
|
}
|
|
251
269
|
let r = t.reader;
|
|
252
|
-
return r === null ?
|
|
270
|
+
return r === null ? p(t).foldM((n) => S.isRetryable(n) ? g(t, e.fail(n)) : e.fail(n), (e) => e === null ? h : (t.attach(e.reader, e.status), _(t))) : m(r).foldM((n) => (t.closeConnection(), S.isRetryable(n) ? g(t, e.fail(n)) : e.fail(n)), (n) => {
|
|
271
|
+
if (n.done) return t.closeConnection(), g(t, h);
|
|
272
|
+
let r;
|
|
273
|
+
try {
|
|
274
|
+
r = t.frames.push(n.value);
|
|
275
|
+
} catch (n) {
|
|
276
|
+
return t.closeConnection(), e.fail(new o(t.status, String(n.message), null, i, void 0, "decode", n));
|
|
277
|
+
}
|
|
278
|
+
for (let e of r) t.pending.push(e);
|
|
279
|
+
return _(t);
|
|
280
|
+
});
|
|
253
281
|
};
|
|
254
|
-
return r.bracket(e.promise(async (e) => new
|
|
255
|
-
} },
|
|
282
|
+
return r.bracket(e.promise(async (e) => new N(d, e)), (e) => r.unfoldEval(e, _), (t) => e.sync(() => t.close()));
|
|
283
|
+
} }, I = class {
|
|
256
284
|
interceptors;
|
|
257
285
|
baseUrl;
|
|
258
286
|
defaultHeaders;
|
|
@@ -288,17 +316,17 @@ var i = {
|
|
|
288
316
|
fetch(e, t, n = {}) {
|
|
289
317
|
return this.request(e, t, n);
|
|
290
318
|
}
|
|
291
|
-
send(t, n, r
|
|
292
|
-
let i = this.resolveUrl(n);
|
|
293
|
-
return this.withTimeout(i,
|
|
319
|
+
send(t, n, r) {
|
|
320
|
+
let i = this.resolveUrl(n), a = r ?? {};
|
|
321
|
+
return this.withTimeout(i, a.timeout, this.exchange(i, t, a).flatMap((t) => t.ok ? e.sync(() => void t.body?.cancel().catch(() => {})) : b.readFailure(t, "json", i)));
|
|
294
322
|
}
|
|
295
|
-
raw(t, n, r
|
|
296
|
-
let i = this.resolveUrl(n);
|
|
297
|
-
return this.withTimeout(i,
|
|
323
|
+
raw(t, n, r) {
|
|
324
|
+
let i = this.resolveUrl(n), a = r ?? {};
|
|
325
|
+
return this.withTimeout(i, a.timeout, this.exchange(i, t, a).flatMap((t) => t.ok ? e.of(t) : b.readFailure(t, "json", i)));
|
|
298
326
|
}
|
|
299
327
|
sse(e, t = {}) {
|
|
300
328
|
if (t.retry !== void 0 && !(t.retry >= 0)) throw RangeError(`HttpClient.sse: retry must be a non-negative number of milliseconds, got ${t.retry}`);
|
|
301
|
-
return
|
|
329
|
+
return F.stream({
|
|
302
330
|
url: this.resolveUrl(e),
|
|
303
331
|
headers: t.headers ?? {},
|
|
304
332
|
defaultHeaders: this.defaultHeaders,
|
|
@@ -317,21 +345,20 @@ var i = {
|
|
|
317
345
|
headers: this.defaultHeaders,
|
|
318
346
|
credentials: this.defaultCredentials
|
|
319
347
|
};
|
|
320
|
-
return e.lift(() =>
|
|
348
|
+
return e.lift(() => f.prepare(n, r, i), (e) => o.request(t, e)).flatMap((n) => e.cancellable((e) => C.run(this.interceptors, C.request(t, {
|
|
321
349
|
...n,
|
|
322
350
|
signal: e
|
|
323
|
-
}),
|
|
351
|
+
}), C.fetch), (e) => S.error(e, t)));
|
|
324
352
|
}
|
|
325
353
|
withTimeout(e, t, n) {
|
|
326
354
|
let r = t ?? this.defaultTimeout;
|
|
327
|
-
return r == null ? n : n.timeout(r, () =>
|
|
355
|
+
return r == null ? n : n.timeout(r, () => o.timeout(e, r));
|
|
328
356
|
}
|
|
329
357
|
request(t, r, i) {
|
|
330
|
-
let
|
|
331
|
-
return this.withTimeout(
|
|
358
|
+
let a = this.resolveUrl(t), { responseType: s = "json", transform: c = (e) => e, empty: l = "error" } = i;
|
|
359
|
+
return this.withTimeout(a, i.timeout, this.exchange(a, r, i).flatMap((t) => t.ok ? b.read(t, s, a, r).flatMap((r) => r === _ ? l === "option" ? e.of(n.empty()) : e.fail(o.decode(a, t, `expected a response body, got none (status ${t.status}); use send() or { empty: "option" }`)) : e.lift(() => c(r), (e) => o.decode(a, t, e)).map((e) => l === "option" ? n.ofNullable(e) : e)) : b.readFailure(t, s, a)));
|
|
332
360
|
}
|
|
333
361
|
};
|
|
334
|
-
|
|
335
|
-
export { N as HttpClient, a as HttpError, E as decodeEvents };
|
|
362
|
+
export { I as HttpClient, o as HttpError, D as decodeEvents };
|
|
336
363
|
|
|
337
364
|
//# sourceMappingURL=monadyssey-fetch.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monadyssey-fetch.mjs","names":[],"sources":["../src/http-error.ts","../src/http-request.ts","../src/http-response.ts","../src/http-transport.ts","../src/interceptors.ts","../src/sse.ts","../src/sse-client.ts","../src/http-client.ts"],"sourcesContent":["import { Option } from \"@openstage/monadyssey-core\";\n\n/**\n * How a request failed.\n * - `\"request\"`: the request could not be built — typically the body could not be serialized.\n * Nothing was sent.\n * - `\"response\"`: the server answered with a non-2xx status. `body` carries what it sent.\n * - `\"decode\"`: the server answered 2xx, but the body could not be used — it failed to parse, a\n * `transform` threw, or there was no body where one was expected (a `204`, say). `status` is the\n * real status and `cause` the underlying error.\n * - `\"network\"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.\n * - `\"timeout\"`: the client's `timeout` option fired before the response, or its body, completed.\n * The request may have reached the server.\n * - `\"aborted\"`: the request was aborted by something other than IO cancellation, such as an\n * interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as\n * `Cancelled`, not as an `HttpError`).\n *\n * New kinds may be added in a minor release, so give an exhaustive `switch` a `default` branch.\n */\nexport type HttpErrorKind = \"request\" | \"response\" | \"decode\" | \"network\" | \"timeout\" | \"aborted\";\n\n/** @internal */\nexport const HttpErrorMessage = {\n describe: (cause: unknown): string =>\n cause instanceof Error\n ? cause.message\n : typeof cause === \"string\"\n ? cause\n : typeof cause === \"object\" && cause !== null && \"message\" in cause\n ? String((cause as { message: unknown }).message)\n : \"An unknown error occurred.\",\n\n /** Response errors keep the pre-3.0 wording; the other kinds carry no status. */\n format: (kind: HttpErrorKind, url: string, status: number, rawMessage: string, timeoutMs?: number): string => {\n switch (kind) {\n case \"response\":\n return `Request to '${url}' failed with status ${status} and message: ${rawMessage}.`;\n case \"decode\":\n return `Response from '${url}' (status ${status}) could not be decoded: ${rawMessage}.`;\n case \"request\":\n return `Request to '${url}' could not be sent: ${rawMessage}.`;\n case \"timeout\":\n return `Request to '${url}' timed out after ${timeoutMs} ms.`;\n case \"aborted\":\n return `Request to '${url}' was aborted before a response arrived: ${rawMessage}.`;\n default:\n return `Request to '${url}' failed before a response arrived: ${rawMessage}.`;\n }\n },\n};\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP\n * status code, raw error message, response body, request URL, response headers, and the\n * underlying cause.\n *\n * `kind` tells the two facts apart that `status` alone cannot: \"the server said 500\" is\n * `kind: \"response\", status: 500`, while \"nothing answered\" is `kind: \"network\", status: 0`.\n * For every kind other than `\"response\"` and `\"decode\"`, `status` is `0`; `body` is `null` except\n * for `\"response\"`.\n *\n * @example\n * client.post(\"/payments\", payment).mapErr((error) =>\n * error.kind === \"response\"\n * ? { kind: \"rejected\", status: error.status }\n * : { kind: \"outcomeUnknown\" } // the request may or may not have reached the server\n * );\n */\nexport class HttpError extends Error {\n /** The transport outcome. See {@link HttpErrorKind}. */\n public readonly kind: HttpErrorKind;\n /** The HTTP status code, or `0` when no response arrived (`kind !== \"response\"`). */\n public readonly status: number;\n public readonly rawMessage: string;\n /** The parsed response body, or `null` when none was parsed. */\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n /** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */\n public readonly cause?: unknown;\n /** The timeout that fired, in milliseconds. Set only when `kind === \"timeout\"`. */\n public readonly timeoutMs?: number;\n\n constructor(\n status: number,\n rawMessage: string,\n body: unknown,\n url: string,\n headers?: Record<string, string>,\n kind: HttpErrorKind = \"response\",\n cause?: unknown,\n timeoutMs?: number\n ) {\n super(HttpErrorMessage.format(kind, url, status, rawMessage, timeoutMs));\n this.name = \"HttpError\";\n this.kind = kind;\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\n this.cause = cause;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * An error for a request that received no response: DNS failure, offline, CORS block, or a\n * rejected interceptor. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).\n */\n static network(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"network\", cause);\n }\n\n /**\n * An error for a request whose `timeout` option fired before the response, or its body, completed.\n * `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.\n *\n * @param {string} url The request URL.\n * @param {number} after The timeout in milliseconds.\n */\n static timeout(url: string, after: number): HttpError {\n return new HttpError(0, `Request timed out after ${after} ms`, null, url, undefined, \"timeout\", undefined, after);\n }\n\n /**\n * An error for a 2xx response whose body could not be used: it failed to parse, a `transform`\n * threw, or there was no body where one was expected. `status` is the real status, `body` is\n * `null`, and `cause` carries the underlying error.\n *\n * @param {string} url The request URL.\n * @param {Response} response The response that could not be decoded.\n * @param {unknown} cause The parse error, the value a `transform` threw, or a description.\n */\n static decode(url: string, response: Response, cause: unknown): HttpError {\n let headers: Record<string, string> = {};\n try {\n headers = Object.fromEntries(response.headers.entries());\n } catch {\n /* headers unavailable */\n }\n return new HttpError(response.status, HttpErrorMessage.describe(cause), null, url, headers, \"decode\", cause);\n }\n\n /**\n * An error for a request that could not be built, so nothing was sent — typically a body that\n * could not be serialized. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).\n */\n static request(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"request\", cause);\n }\n\n /**\n * An error for a request aborted by something other than IO cancellation, such as an\n * interceptor aborting the signal. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} [cause] The abort reason, if any.\n */\n static aborted(url: string, cause?: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"aborted\", cause);\n }\n\n /**\n * Narrows the `unknown` error body to a concrete type using a runtime type guard.\n * Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,\n * not asserted. This keeps the unsafe cast out of consumer code: the body stays `unknown`\n * by default (forcing a conscious narrow), and this is the safe way to perform it.\n *\n * @template T The expected body shape.\n * @param {(x: unknown) => x is T} guard A type guard validating the body's shape.\n * @returns {Option<T>} `Some(body)` if the guard matches, `None` otherwise.\n *\n * @example\n * interface ApiError { code: string; message: string }\n * const isApiError = (x: unknown): x is ApiError =>\n * typeof x === \"object\" && x !== null && \"code\" in x && \"message\" in x;\n *\n * httpError.bodyAs(isApiError).fold(\n * () => \"unrecognized error shape\",\n * (e) => `${e.code}: ${e.message}`\n * );\n */\n bodyAs<T>(guard: (x: unknown) => x is T): Option<NonNullable<T>> {\n return guard(this.body) ? Option.ofNullable(this.body) : Option.empty();\n }\n}\n","import { Credentials, Method, Options } from \"./options\";\n\n/** @internal */\nexport interface RequestDefaults {\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n}\n\n/** @internal */\nexport interface PreparedRequest {\n readonly method: Method;\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n readonly body: BodyInit | undefined;\n}\n\n/** Bodies `fetch` sends as-is: never JSON-encoded, never given an automatic content type. */\nconst isNativeBody = (body: unknown): boolean =>\n typeof body !== \"object\" ||\n body === null ||\n body instanceof FormData ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof URLSearchParams ||\n (typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream) ||\n ArrayBuffer.isView(body);\n\nconst normalizeHeaders = (headers: Record<string, string | undefined>): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([key, value]) => [key.toLowerCase(), value])\n );\n\n/** `FormData` must carry no content type so the browser can set the multipart boundary. */\nconst withContentType = (headers: Record<string, string>, body: unknown): Record<string, string> => {\n if (body instanceof FormData) {\n const { \"content-type\": _dropped, ...rest } = headers;\n return rest;\n }\n const autoJson = body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in headers);\n return autoJson ? { ...headers, \"content-type\": \"application/json\" } : headers;\n};\n\n/**\n * `application/json`, with or without parameters (`; charset=utf-8`), or any `+json` suffix type\n * (`application/merge-patch+json`, `application/vnd.api+json`). Matching the exact string alone\n * would send an object declared as one of the others as `\"[object Object]\"`.\n */\nconst isJsonContentType = (contentType: string | undefined): boolean => {\n if (contentType === undefined) return false;\n const media = contentType.split(\";\")[0].trim().toLowerCase();\n return media === \"application/json\" || media.endsWith(\"+json\");\n};\n\nconst serializeBody = (method: Method, body: unknown, headers: Record<string, string>): BodyInit | undefined =>\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? (body as BodyInit)\n : isJsonContentType(headers[\"content-type\"])\n ? JSON.stringify(body)\n : (body as BodyInit);\n\n/** @internal */\nexport const HttpRequest = {\n isNativeBody,\n normalizeHeaders,\n\n /** Per-request headers win over defaults; names are lowercased; the body follows the content type. */\n prepare: (method: Method, options: Options<unknown>, defaults: RequestDefaults): PreparedRequest => {\n const headers = withContentType(\n normalizeHeaders({ ...defaults.headers, ...(options.headers ?? {}) }),\n options.body\n );\n return {\n method,\n headers,\n credentials: options.credentials ?? defaults.credentials,\n body: serializeBody(method, options.body, headers),\n };\n },\n};\n","import { IO, Option } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { Method, ResponseType } from \"./options\";\n\ntype BodyReader = (response: Response) => Promise<unknown>;\n\nconst bodyReaders = {\n json: (response) => response.json(),\n text: (response) => response.text(),\n blob: (response) => response.blob(),\n arrayBuffer: (response) => response.arrayBuffer(),\n formData: (response) => response.formData(),\n} satisfies Record<ResponseType, BodyReader>;\n\n/** An own-property lookup, so an untyped `responseType` such as `\"toString\"` is rejected. */\nconst readerFor = (responseType: ResponseType): Option<BodyReader> =>\n Object.prototype.hasOwnProperty.call(bodyReaders, responseType)\n ? Option.of(bodyReaders[responseType])\n : Option.empty<BodyReader>();\n\nconst headers = (response: Response): Record<string, string> => {\n try {\n return Object.fromEntries(response.headers.entries());\n } catch {\n return {};\n }\n};\n\n/** The server answered 2xx but the body could not be used (`kind: \"decode\"`). */\nconst error = (cause: unknown, url: string, response: Response): HttpError => HttpError.decode(url, response, cause);\n\n/**\n * What {@link HttpResponse.read} yields when there is no body at all — distinct from a JSON body\n * that is literally `null`.\n *\n * @internal\n */\nexport const NO_BODY: unique symbol = Symbol(\"HttpResponse.noBody\");\n\n/** The server answered with a non-2xx status; the parsed body travels with the error. */\nconst failure = (response: Response, body: unknown, url: string): HttpError =>\n new HttpError(response.status, response.statusText, body, url, headers(response));\n\n/**\n * Reads a JSON body, yielding `NO_BODY` for an empty one. Responses that implement only `json()` —\n * hand-rolled mocks in consumers' tests — are read with it.\n */\nconst readJson = async (response: Response): Promise<unknown> => {\n if (typeof response.text !== \"function\") return response.json();\n const text = await response.text();\n return text.trim() === \"\" ? NO_BODY : JSON.parse(text);\n};\n\n/** @internal */\nexport const HttpResponse = {\n headers,\n error,\n failure,\n\n /**\n * Reads a successful body. Yields `NO_BODY` when there is none to read: `204`/`205`, a `HEAD`\n * request, or an empty body under `\"json\"` (e.g. `201 Created` / `202 Accepted` with nothing\n * attached). A body that cannot be parsed fails with `kind: \"decode\"`.\n */\n read: (\n response: Response,\n responseType: ResponseType,\n url: string,\n method?: Method\n ): IO<HttpError, unknown | typeof NO_BODY> =>\n IO.from(readerFor(responseType), () => error(`Unsupported response type: ${responseType}`, url, response)).flatMap(\n (read) =>\n IO.lift(\n async () => {\n if (response.status === 204 || response.status === 205 || method === \"HEAD\") return NO_BODY;\n if (responseType !== \"json\") return read(response);\n return readJson(response);\n },\n (cause) => error(cause, url, response)\n )\n ),\n\n /**\n * Fails with the error for a non-2xx response. The body is read leniently, so an error page\n * that is not JSON (an HTML 502 from a proxy, say) still travels with the error and the message\n * stays the status text: under `\"json\"` the text is parsed when it is JSON and kept as a string\n * otherwise; an unreadable or empty body is `null`.\n */\n readFailure: (response: Response, responseType: ResponseType, url: string): IO<HttpError, never> =>\n IO.promise<unknown>(async () => {\n try {\n if (responseType !== \"json\") {\n return await readerFor(responseType).fold(\n async () => null,\n (read) => read(response)\n );\n }\n // A response without `text()` (a hand-rolled mock) can only be read as JSON.\n if (typeof response.text !== \"function\") return await response.json();\n const text = await response.text();\n if (text.trim() === \"\") return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n } catch {\n return null;\n }\n }).flatMap((body) => IO.fail(failure(response, body, url))),\n};\n","import { HttpError } from \"./http-error\";\n\nconst isAbort = (e: unknown): boolean =>\n typeof e === \"object\" && e !== null && \"name\" in e && (e as { name: unknown }).name === \"AbortError\";\n\n/** @internal */\nexport const HttpTransport = {\n /** An interceptor may reject with a ready-made `HttpError`; it is passed through untouched. */\n error: (e: unknown, url: string): HttpError =>\n e instanceof HttpError ? e : isAbort(e) ? HttpError.aborted(url, e) : HttpError.network(url, e),\n\n /** A timeout means the caller stopped waiting and an abort means someone wanted it stopped: neither is retried. */\n isRetryable: (error: HttpError): boolean =>\n error.kind === \"network\" || (error.kind === \"response\" && error.status >= 500),\n};\n","import { HttpInterceptor, InterceptedRequest } from \"./options\";\n\n/** @internal */\nexport type Send = (request: InterceptedRequest) => Promise<Response>;\n\n/** @internal */\nexport const Interceptors = {\n /** A right fold, so the first registered interceptor is outermost. */\n run: (interceptors: readonly HttpInterceptor[], request: InterceptedRequest, send: Send): Promise<Response> =>\n interceptors.reduceRight<Send>((next, interceptor) => (r) => interceptor.intercept(r, next), send)(request),\n\n /** Builds the request interceptors see: headers normalised to a `Headers` instance. */\n request: (\n url: string,\n init: Omit<RequestInit, \"headers\"> & { headers: Record<string, string> }\n ): InterceptedRequest => ({\n url,\n init: { ...init, headers: new Headers(init.headers) },\n }),\n\n /** The end of the chain. */\n fetch: (request: InterceptedRequest): Promise<Response> => fetch(request.url, request.init),\n};\n","import { List, Stream } from \"@openstage/monadyssey-core\";\nimport type { Pipe } from \"@openstage/monadyssey-core\";\n\n/**\n * A single server-sent event parsed from a text/event-stream response.\n *\n * @property id - The last event id in effect when the event was dispatched — set by the event's\n * own `id:` field or by an earlier one, as the spec's `MessageEvent.lastEventId`. Absent until\n * the server sends an `id:`; the empty string after the server resets it. Used to resume a stream.\n * @property event - The event type. Defaults to `\"message\"` when the server sends no `event:` field.\n * @property data - The event payload. Multiple `data:` lines are joined with a newline.\n * @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.\n */\nexport interface ServerSentEvent {\n readonly id?: string;\n readonly event: string;\n readonly data: string;\n readonly retry?: number;\n}\n\n/**\n * The effects of one block (the lines up to a blank line). A block may set the last event id or\n * the reconnection time without dispatching an event — e.g. a lone `retry: 5000` or `id: 42`.\n *\n * @internal\n */\nexport interface SseFrame {\n /** The event to dispatch, or `null` when the block has no `data:` field. */\n readonly event: ServerSentEvent | null;\n /** The last `id:` value in the block, if any. The empty string resets the last event id. */\n readonly id?: string;\n /** The last valid `retry:` value in the block, if any. */\n readonly retry?: number;\n}\n\nconst parseBlock = (block: string): SseFrame => {\n let event = \"\";\n let id: string | undefined;\n let retry: number | undefined;\n const data: string[] = [];\n\n for (const line of block.split(\"\\n\")) {\n if (line === \"\" || line.startsWith(\":\")) continue; // blank line or comment\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n\n switch (field) {\n case \"event\":\n event = value;\n break;\n case \"data\":\n data.push(value);\n break;\n case \"id\":\n // Per the spec, an id containing NUL is ignored.\n if (!value.includes(\"\\0\")) id = value;\n break;\n case \"retry\":\n if (/^\\d+$/.test(value)) retry = Number.parseInt(value, 10);\n break;\n default:\n break;\n }\n }\n\n return {\n event:\n data.length === 0\n ? null\n : {\n // An empty `event:` field means the default type, not the empty string.\n event: event || \"message\",\n data: data.join(\"\\n\"),\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n },\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n };\n};\n\n/**\n * `event` with `id` set to the last event id in effect (the spec's `lastEventId`), which persists\n * across events until the server changes it.\n *\n * @internal\n */\nexport const withLastEventId = (event: ServerSentEvent, lastEventId: string | undefined): ServerSentEvent => {\n const { id: _blockId, ...rest } = event;\n return lastEventId === undefined ? rest : { ...rest, id: lastEventId };\n};\n\n/**\n * Reassembles a byte stream into complete blocks; partial input and split multi-byte characters\n * are buffered. Call `reset` when a new connection begins.\n *\n * @internal\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n /** The previous chunk ended in CR: a LF opening the next chunk belongs to that CRLF. */\n private skipLeadingLF = false;\n\n push(chunk: Uint8Array): SseFrame[] {\n let text = this.decoder.decode(chunk, { stream: true });\n if (text.length === 0) return [];\n if (this.skipLeadingLF && text.startsWith(\"\\n\")) text = text.slice(1);\n // A trailing CR ends its line now — holding it back until the next chunk would delay every\n // event on CR-only streams by a chunk. If a LF follows, it is skipped above.\n this.skipLeadingLF = text.endsWith(\"\\r\");\n text = this.buffered + text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n const blocks = text.split(\"\\n\\n\");\n this.buffered = blocks.pop() ?? \"\";\n return blocks.map(parseBlock);\n }\n\n reset(): void {\n this.decoder = new TextDecoder(\"utf-8\");\n this.buffered = \"\";\n this.skipLeadingLF = false;\n }\n}\n\n/**\n * A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering\n * partial input across chunks. Single-use: each application maintains its own decoding state.\n *\n * @typeParam E - The error type carried by the surrounding stream.\n * @returns A pipe from a byte stream to a stream of parsed events.\n */\nexport const decodeEvents = <E>(): Pipe<E, Uint8Array, ServerSentEvent> => {\n const frames = new FrameBuffer();\n return (bytes: Stream<E, Uint8Array>) =>\n bytes.mapAccumulate<string | undefined, ServerSentEvent>(undefined, (lastEventId, chunk) => {\n const events: ServerSentEvent[] = [];\n let current = lastEventId;\n for (const frame of frames.push(chunk)) {\n if (frame.id !== undefined) current = frame.id;\n if (frame.event !== null) events.push(withLastEventId(frame.event, current));\n }\n return [current, List.from(events)] as const;\n });\n};\n","import { IO, Option, Schedule, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { Credentials, HttpInterceptor } from \"./options\";\nimport { FrameBuffer, ServerSentEvent, SseFrame, withLastEventId } from \"./sse\";\n\n/** A server-sent `retry:` below this is raised to it, so a misbehaving server cannot force a tight loop. */\nconst MIN_SERVER_RETRY_MS = 100;\n\n/** Largest delay `setTimeout` honours; anything above it (or Infinity) fires after ~1ms. */\nconst MAX_DELAY_MS = 2_147_483_647;\n\ntype Reader = ReadableStreamDefaultReader<Uint8Array>;\n\n/** A cancellable sleep. The abort listener is removed when the timer fires, so waits do not accumulate listeners. */\nconst delay = (ms: number): IO<never, void> =>\n IO.promise<void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(\n () => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n },\n Math.min(ms, MAX_DELAY_MS)\n );\n signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/**\n * Carried across reconnections within one run. Owns the one `AbortController` every connection\n * of the run is opened with, linked once to the run's signal; a connection registers a single\n * listener on it and removes it when closed, so listeners stay bounded however long the run lasts.\n */\nclass SseSession {\n readonly controller = new AbortController();\n readonly frames = new FrameBuffer();\n readonly pending: SseFrame[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n /** The server's `retry:` value, once received; it takes precedence over a `Schedule`'s delays. */\n serverRetryMs: number | undefined = undefined;\n /** Consecutive reconnects since the last delivered event. */\n reconnects = 0;\n private connection: { readonly reader: Reader; readonly unlink: () => void } | null = null;\n private readonly unlinkParent: () => void;\n\n constructor(retryMs: number, parent: AbortSignal) {\n this.retryMs = retryMs;\n const onAbort = () => this.controller.abort(parent.reason);\n parent.addEventListener(\"abort\", onAbort, { once: true });\n this.unlinkParent = () => parent.removeEventListener(\"abort\", onAbort);\n }\n\n get reader(): Reader | null {\n return this.connection?.reader ?? null;\n }\n\n attach(reader: Reader): void {\n const onAbort = () => reader.cancel().catch(() => {});\n this.controller.signal.addEventListener(\"abort\", onAbort, { once: true });\n this.connection = { reader, unlink: () => this.controller.signal.removeEventListener(\"abort\", onAbort) };\n this.frames.reset();\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.unlink();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n this.unlinkParent();\n this.controller.abort();\n }\n}\n\n/** @internal */\nexport interface SseConfig {\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly defaultHeaders: Record<string, string>;\n readonly credentials: Credentials;\n readonly interceptors: readonly HttpInterceptor[];\n readonly reconnect: boolean | Schedule;\n readonly retry: number;\n}\n\nconst mediaType = (contentType: string): string => contentType.split(\";\")[0].trim().toLowerCase();\n\nconst stream = (config: SseConfig): Stream<HttpError, ServerSentEvent> => {\n const { url, headers, defaultHeaders, credentials, interceptors, reconnect, retry } = config;\n\n /** The reader for a new connection, or `null` when the server answered 204 (\"stop reconnecting\"). */\n const open = (session: SseSession): IO<HttpError, Reader | null> => {\n const prepared = HttpRequest.prepare(\n \"GET\",\n {\n headers: {\n ...headers,\n accept: \"text/event-stream\",\n // An empty id is how a server resets it: the header is sent only for a non-empty id.\n ...(session.lastEventId ? { \"last-event-id\": session.lastEventId } : {}),\n },\n credentials,\n },\n { headers: defaultHeaders, credentials }\n );\n return IO.lift<HttpError, Response>(\n () =>\n Interceptors.run(\n interceptors,\n Interceptors.request(url, { ...prepared, signal: session.controller.signal }),\n Interceptors.fetch\n ),\n (e: unknown) => HttpTransport.error(e, url)\n )\n .flatMap((response) => (response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"text\", url)))\n .flatMap((response): IO<HttpError, Reader | null> => {\n if (response.status === 204) {\n void response.body?.cancel().catch(() => {});\n return IO.of(null);\n }\n // Not an event stream (e.g. an HTML login page or SPA fallback): fail without\n // reconnecting, instead of parsing nothing and reconnecting forever. A missing\n // content type is tolerated.\n const contentType = response.headers.get(\"content-type\");\n if (contentType !== null && mediaType(contentType) !== \"text/event-stream\") {\n void response.body?.cancel().catch(() => {});\n return IO.fail(HttpResponse.error(`Expected text/event-stream, got ${contentType}`, url, response));\n }\n return response.body === null\n ? IO.fail<HttpError, Reader>(HttpResponse.error(\"Response has no body\", url, response))\n : IO.of(response.body.getReader());\n });\n };\n\n const readChunk = (reader: Reader): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.lift(\n () => reader.read(),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\n const done = IO.of<EventStep>(Option.empty());\n\n /**\n * Reconnects after the scheduled delay, or gives up (`giveUp`) when reconnection is off or the\n * `Schedule` allows no further attempt. A server `retry:` value, once received, is the delay.\n */\n const reconnectOr = (session: SseSession, giveUp: IO<HttpError, EventStep>): IO<HttpError, EventStep> => {\n if (reconnect === false) return giveUp;\n const made = session.reconnects + 1;\n if (reconnect !== true && !reconnect.allowsAttempt(made)) return giveUp;\n const ms = session.serverRetryMs ?? (reconnect === true ? session.retryMs : reconnect.delayAfter(made));\n session.reconnects = made;\n return delay(ms).flatMap(() => step(session));\n };\n\n const step = (session: SseSession): IO<HttpError, EventStep> => {\n while (session.pending.length > 0) {\n const frame = session.pending.shift() as SseFrame;\n // `id:` and `retry:` take effect even in a block that dispatches no event.\n if (frame.id !== undefined) session.lastEventId = frame.id;\n if (frame.retry !== undefined) session.serverRetryMs = Math.max(frame.retry, MIN_SERVER_RETRY_MS);\n if (frame.event !== null) {\n session.reconnects = 0;\n return IO.of(\n Option.of<[ServerSentEvent, SseSession]>([withLastEventId(frame.event, session.lastEventId), session])\n );\n }\n }\n\n const reader = session.reader;\n if (reader === null) {\n return open(session).foldM<HttpError, EventStep>(\n (error) => (HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error)),\n (opened) => {\n if (opened === null) return done;\n session.attach(opened);\n return step(session);\n }\n );\n }\n\n return readChunk(reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n return HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n return reconnectOr(session, done);\n }\n session.pending.push(...session.frames.push(result.value));\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.promise(async (signal: AbortSignal) => new SseSession(retry, signal)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.sync(() => session.close())\n );\n};\n\n/** @internal */\nexport const SseClient = { stream };\n","import { IO, Option, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest, PreparedRequest } from \"./http-request\";\nimport { HttpResponse, NO_BODY } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { SseClient } from \"./sse-client\";\nimport { Credentials, HttpClientConfig, HttpInterceptor, Method, Options, SendOptions, SseOptions } from \"./options\";\nimport { ServerSentEvent } from \"./sse\";\n\n/** Options for a request that expects no body back, or `Option` of one. */\ntype OptionalBody<A> = Options<A> & { empty: \"option\" };\n/** Options for a request whose body is required (the default). */\ntype RequiredBody<A> = Options<A> & { empty?: \"error\" };\n\n/**\n * A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of\n * Promises. Each instance carries its own configuration (base URL, interceptors, default headers,\n * timeout, credentials), so different parts of an application can use independently configured\n * clients.\n *\n * Three kinds of call, by what you want back:\n * - **the decoded body** — `get`, `post`, `put`, `patch`, `delete`, `fetch` return\n * `IO<HttpError, A>`. A response with no body (`204`, an empty `200`) fails with\n * `kind: \"decode\"`, since a value was expected; pass `{ empty: \"option\" }` when absence is\n * legitimate, to get `IO<HttpError, Option<A>>`.\n * - **nothing** — `send(method, uri, { body })` returns `IO<HttpError, void>` for actions whose\n * response body you do not read (publish, archive, leave …); any body, or none, is fine.\n * - **the `Response`** — `raw(method, uri, options?)` returns `IO<HttpError, Response>` for\n * status codes, headers or streaming.\n *\n * Every non-2xx response fails with `kind: \"response\"`, whichever you use. Cancellation is\n * supported: when the IO is cancelled (via a fiber or `timeout`), the underlying `fetch` is\n * aborted through its `AbortSignal`.\n *\n * `A` is what you claim the body is; the JSON is not checked unless you pass a `transform` that\n * validates it (a schema's `parse`, say) — a throw there fails with `kind: \"decode\"`.\n *\n * **Default credentials:** `same-origin`, matching the platform `fetch` default. Set\n * `credentials: \"include\"` explicitly per-client or per-request if you need cookies to flow\n * cross-origin.\n *\n * @example\n * const api = new HttpClient({\n * baseUrl: \"https://api.example.com\",\n * interceptors: [authInterceptor],\n * timeout: 5000,\n * });\n *\n * const users = api.get<User[]>(\"/users\"); // IO<HttpError, User[]>\n * const avatar = api.get<Avatar>(\"/me/avatar\", { empty: \"option\" }); // IO<HttpError, Option<Avatar>>\n * const leave = api.send(\"POST\", `/communities/${slug}/leave`); // IO<HttpError, void>\n */\nexport class HttpClient {\n private readonly interceptors: readonly HttpInterceptor[];\n private readonly baseUrl: string | undefined;\n private readonly defaultHeaders: Record<string, string>;\n private readonly defaultTimeout: number | undefined;\n private readonly defaultCredentials: Credentials;\n\n /**\n * Creates a new HttpClient with the given configuration.\n *\n * @param {HttpClientConfig} config - Configuration for the client.\n */\n constructor(config: HttpClientConfig = {}) {\n this.interceptors = Object.freeze([...(config.interceptors ?? [])]);\n this.baseUrl = config.baseUrl;\n this.defaultHeaders = config.defaultHeaders ?? {};\n this.defaultTimeout = config.timeout;\n this.defaultCredentials = config.credentials ?? \"same-origin\";\n }\n\n /**\n * Performs a GET request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n get<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n get<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n get<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"GET\", options);\n }\n\n /**\n * Performs a POST request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n post<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n post<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n post<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"POST\", { ...options, body });\n }\n\n /**\n * Performs a PUT request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n put<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n put<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n put<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PUT\", { ...options, body });\n }\n\n /**\n * Performs a PATCH request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n patch<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n patch<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n patch<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PATCH\", { ...options, body });\n }\n\n /**\n * Performs a DELETE request and decodes the body. For a DELETE that answers `204 No Content`,\n * use `send(\"DELETE\", uri)` instead.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n delete<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n delete<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n delete<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"DELETE\", options);\n }\n\n /**\n * Performs a request with any method and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Method} method - The HTTP method.\n * @param {Options<A>} [options] - Request options, including `body`; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n fetch<A = unknown>(uri: string, method: Method, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n fetch<A = unknown>(uri: string, method: Method, options?: RequiredBody<A>): IO<HttpError, A>;\n fetch<A = unknown>(uri: string, method: Method, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, method, options);\n }\n\n /**\n * Performs a request whose response body you do not need — an action such as publish,\n * archive or leave. Succeeds with `void` for any 2xx, with or without a body (which is\n * discarded); a non-2xx response fails with `kind: \"response\"` and its body.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body. A `GET` or `HEAD`\n * request with a body throws `TypeError` where it is built.\n * @returns {IO<HttpError, void>}\n *\n * @example\n * client.send(\"POST\", `/api/v1/communities/${slug}/leave`);\n * client.send(\"PUT\", `/api/v1/articles/${id}`, { body: article });\n * client.send(\"DELETE\", `/api/v1/passkeys/${id}`, { headers: { \"X-Step-Up\": token } });\n */\n send(method: Method, uri: string, options: SendOptions = {}): IO<HttpError, void> {\n const url = this.resolveUrl(uri);\n return this.withTimeout(\n url,\n options.timeout,\n this.exchange(url, method, options).flatMap((response) =>\n response.ok\n ? IO.sync(() => void response.body?.cancel().catch(() => {}))\n : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Performs a request and yields the `Response` itself, for status codes, headers or streaming.\n * A non-2xx response still fails with `kind: \"response\"` (its body read into the error); a 2xx\n * `Response` is returned unread. When you only need the status or headers, cancel the body\n * (`response.body?.cancel()`) so the connection is released; `send` does this for you.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body.\n * @returns {IO<HttpError, Response>}\n *\n * @example\n * client.raw(\"POST\", \"/api/v1/communities\", { body: input }).map((r) => r.headers.get(\"Location\"));\n */\n raw(method: Method, uri: string, options: SendOptions = {}): IO<HttpError, Response> {\n const url = this.resolveUrl(uri);\n return this.withTimeout(\n url,\n options.timeout,\n this.exchange(url, method, options).flatMap((response) =>\n response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.\n *\n * The request goes through the same pipeline as every other method, so headers (including\n * `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is\n * used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts\n * the connection and releases resources.\n *\n * By default the stream reconnects automatically when the connection ends or fails with a\n * retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers\n * and credentials on each attempt; terminal `4xx` responses (including auth failures) end the\n * stream. See {@link SseOptions} to configure or disable this.\n *\n * @param {string} uri - The URL or path to connect to.\n * @param {SseOptions} [options] - Headers, credentials, and reconnection settings.\n * @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.\n * @throws {RangeError} If `options.retry` is negative or `NaN`.\n *\n * @example\n * const events = client.sse(\"/events\", { headers: { Authorization: `Bearer ${token}` } });\n * await events.runForEach((event) => IO.sync(() => handle(event))).unsafeRun();\n */\n sse(uri: string, options: SseOptions = {}): Stream<HttpError, ServerSentEvent> {\n // A negative or NaN delay is a programming error; left through, `setTimeout` would treat it\n // as 0 and reconnect in a tight loop.\n if (options.retry !== undefined && !(options.retry >= 0)) {\n throw new RangeError(`HttpClient.sse: retry must be a non-negative number of milliseconds, got ${options.retry}`);\n }\n return SseClient.stream({\n url: this.resolveUrl(uri),\n headers: options.headers ?? {},\n defaultHeaders: this.defaultHeaders,\n credentials: options.credentials ?? this.defaultCredentials,\n interceptors: this.interceptors,\n reconnect: options.reconnect ?? true,\n retry: options.retry ?? 3000,\n });\n }\n\n private resolveUrl(uri: string): string {\n // Absolute only with a scheme followed by `//` (or a data:/blob: URL). `new URL()` also\n // accepts `items:batchGet` or `localhost:3000` as absolute, which would bypass `baseUrl`.\n if (!this.baseUrl || /^[a-z][a-z\\d+.-]*:\\/\\//i.test(uri) || /^(data|blob):/i.test(uri)) return uri;\n const base = this.baseUrl.endsWith(\"/\") ? this.baseUrl.slice(0, -1) : this.baseUrl;\n const path = uri.startsWith(\"/\") ? uri : `/${uri}`;\n return `${base}${path}`;\n }\n\n /** Prepares the request, runs it through the interceptors and `fetch`, and yields the `Response`. */\n private exchange(url: string, method: Method, options: SendOptions): IO<HttpError, Response> {\n if ((method === \"GET\" || method === \"HEAD\") && options.body != null) {\n // A programming error, so it throws where the request is built instead of being dropped.\n throw new TypeError(`A ${method} request cannot have a body (${url})`);\n }\n const defaults = { headers: this.defaultHeaders, credentials: this.defaultCredentials };\n const prepare: IO<HttpError, PreparedRequest> = IO.lift(\n () => HttpRequest.prepare(method, options, defaults),\n (e: unknown) => HttpError.request(url, e)\n );\n return prepare.flatMap((prepared) =>\n IO.cancellable(\n (signal: AbortSignal) =>\n Interceptors.run(this.interceptors, Interceptors.request(url, { ...prepared, signal }), Interceptors.fetch),\n (e: unknown) => HttpTransport.error(e, url)\n )\n );\n }\n\n private withTimeout<A>(url: string, timeout: number | undefined, io: IO<HttpError, A>): IO<HttpError, A> {\n const ms = timeout ?? this.defaultTimeout;\n return ms == null ? io : io.timeout(ms, () => HttpError.timeout(url, ms));\n }\n\n private request<A>(uri: string, method: Method, options: Options<A>): IO<HttpError, A | Option<NonNullable<A>>> {\n const url = this.resolveUrl(uri);\n const { responseType = \"json\", transform = (data: unknown) => data as A, empty = \"error\" } = options;\n\n const decode = (response: Response): IO<HttpError, A | Option<NonNullable<A>>> =>\n response.ok\n ? HttpResponse.read(response, responseType, url, method).flatMap((parsed) => {\n if (parsed === NO_BODY) {\n return empty === \"option\"\n ? IO.of(Option.empty<NonNullable<A>>())\n : IO.fail(\n HttpError.decode(\n url,\n response,\n `expected a response body, got none (status ${response.status}); use send() or { empty: \"option\" }`\n )\n );\n }\n return IO.lift<HttpError, A>(\n () => transform(parsed),\n (e: unknown) => HttpError.decode(url, response, e)\n ).map((value) => (empty === \"option\" ? Option.ofNullable(value) : value));\n })\n : HttpResponse.readFailure(response, responseType, url);\n\n return this.withTimeout(url, options.timeout, this.exchange(url, method, options).flatMap(decode));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"monadyssey-fetch.mjs","names":["responseHeaders","response","name","HttpErrorMessage","cause","kind","url","status","rawMessage","timeoutMs","HttpError","body","headers","after","guard","Option","isNativeBody","normalizeHeaders","entry","key","value","withContentType","_dropped","rest","isJsonContentType","media","contentType","serializeBody","method","HttpRequest","defaults","options","bodyReaders","readerFor","responseType","error","NO_BODY","failure","readJson","text","HttpResponse","IO","read","isAbort","HttpTransport","Interceptors","interceptors","interceptor","r","next","send","request","init","e","parseBlock","event","id","retry","data","line","block","colon","field","withLastEventId","_blockId","lastEventId","FrameBuffer","limit","chunk","searchFrom","blocks","decodeEvents","frames","bytes","events","current","frame","List","MIN_SERVER_RETRY_MS","MAX_DELAY_MS","MAX_BLOCK_CHARS","utf8ByteString","b","delay","onAbort","timer","resolve","signal","ms","SseSession","retryMs","parent","reader","mediaType","SseClient","config","open","prepared","session","defaultHeaders","readChunk","done","reconnectOr","reconnect","giveUp","made","step","opened","result","Stream","HttpClient","uri","opts","timeout","io","parsed","empty","transform"],"sources":["../src/http-error.ts","../src/http-request.ts","../src/http-response.ts","../src/http-transport.ts","../src/interceptors.ts","../src/sse.ts","../src/sse-client.ts","../src/http-client.ts"],"sourcesContent":["import { Option } from \"@openstage/monadyssey-core\";\n\n/**\n * How a request failed.\n * - `\"request\"`: the request could not be built — typically the body could not be serialized.\n * Nothing was sent.\n * - `\"response\"`: the server answered with a non-2xx status. `body` carries what it sent.\n * - `\"decode\"`: the server answered 2xx, but the body could not be used — it failed to parse, a\n * `transform` threw, or there was no body where one was expected (a `204`, say). `status` is the\n * real status and `cause` the underlying error.\n * - `\"network\"`: no response arrived — DNS failure, offline, CORS block, or a rejected interceptor.\n * - `\"timeout\"`: the client's `timeout` option fired before the response, or its body, completed.\n * The request may have reached the server.\n * - `\"aborted\"`: the request was aborted by something other than IO cancellation, such as an\n * interceptor's own signal (an IO cancelled via `fiber.cancel()` or `IO.timeout` ends as\n * `Cancelled`, not as an `HttpError`).\n *\n * New kinds may be added in a minor release, so give an exhaustive `switch` a `default` branch.\n */\nexport type HttpErrorKind = \"request\" | \"response\" | \"decode\" | \"network\" | \"timeout\" | \"aborted\";\n\n/**\n * @internal The response headers an `HttpError` carries. `Set-Cookie` is left out: browsers never\n * expose it, Node and Electron do, and an error that ends up in a log must not carry a session.\n */\nexport const responseHeaders = (response: Response): Record<string, string> => {\n try {\n return Object.fromEntries(\n Array.from(response.headers.entries()).filter(([name]) => name !== \"set-cookie\" && name !== \"set-cookie2\")\n );\n } catch {\n return {};\n }\n};\n\n/** @internal */\nexport const HttpErrorMessage = {\n describe: (cause: unknown): string =>\n cause instanceof Error\n ? cause.message\n : typeof cause === \"string\"\n ? cause\n : typeof cause === \"object\" && cause !== null && \"message\" in cause\n ? String((cause as { message: unknown }).message)\n : \"An unknown error occurred.\",\n\n /** Response errors keep the pre-3.0 wording; the other kinds carry no status. */\n format: (kind: HttpErrorKind, url: string, status: number, rawMessage: string, timeoutMs?: number): string => {\n switch (kind) {\n case \"response\":\n return `Request to '${url}' failed with status ${status} and message: ${rawMessage}.`;\n case \"decode\":\n return `Response from '${url}' (status ${status}) could not be decoded: ${rawMessage}.`;\n case \"request\":\n return `Request to '${url}' could not be sent: ${rawMessage}.`;\n case \"timeout\":\n return `Request to '${url}' timed out after ${timeoutMs} ms.`;\n case \"aborted\":\n return `Request to '${url}' was aborted before a response arrived: ${rawMessage}.`;\n default:\n return `Request to '${url}' failed before a response arrived: ${rawMessage}.`;\n }\n },\n};\n\n/**\n * Represents an HTTP error encountered during a request.\n *\n * Extends the native `Error` class with additional context: the transport outcome (`kind`), HTTP\n * status code, raw error message, response body, request URL, response headers, and the\n * underlying cause.\n *\n * `kind` tells the two facts apart that `status` alone cannot: \"the server said 500\" is\n * `kind: \"response\", status: 500`, while \"nothing answered\" is `kind: \"network\", status: 0`.\n * For every kind other than `\"response\"` and `\"decode\"`, `status` is `0`; `body` is `null` except\n * for `\"response\"`.\n *\n * `body`, `headers` and `url` hold what the server and the request contained, which may be sensitive\n * (a token in a query string, an error page echoing input); redact them before logging an error\n * outside a trusted sink. `Set-Cookie` is never carried.\n *\n * @example\n * client.post(\"/payments\", payment).mapErr((error) =>\n * error.kind === \"response\"\n * ? { kind: \"rejected\", status: error.status }\n * : { kind: \"outcomeUnknown\" } // the request may or may not have reached the server\n * );\n */\nexport class HttpError extends Error {\n /** The transport outcome. See {@link HttpErrorKind}. */\n public readonly kind: HttpErrorKind;\n /** The HTTP status code (`\"response\"` and `\"decode\"`), or `0` when no response arrived. */\n public readonly status: number;\n public readonly rawMessage: string;\n /** The parsed response body, or `null` when none was parsed. */\n public readonly body: unknown;\n public readonly url: string;\n public readonly headers?: Record<string, string>;\n /** The underlying exception, when there was one (a `TypeError` from `fetch`, a parse error, an abort reason). */\n public readonly cause?: unknown;\n /** The timeout that fired, in milliseconds. Set only when `kind === \"timeout\"`. */\n public readonly timeoutMs?: number;\n\n constructor(\n status: number,\n rawMessage: string,\n body: unknown,\n url: string,\n headers?: Record<string, string>,\n kind: HttpErrorKind = \"response\",\n cause?: unknown,\n timeoutMs?: number\n ) {\n super(HttpErrorMessage.format(kind, url, status, rawMessage, timeoutMs));\n this.name = \"HttpError\";\n this.kind = kind;\n this.status = status;\n this.rawMessage = rawMessage;\n this.body = body;\n this.url = url;\n this.headers = headers;\n this.cause = cause;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * An error for a request that received no response: DNS failure, offline, CORS block, or a\n * rejected interceptor. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically the `TypeError` thrown by `fetch`).\n */\n static network(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"network\", cause);\n }\n\n /**\n * An error for a request whose `timeout` option fired before the response, or its body, completed.\n * `status` is `0`, `body` is `null`, and `timeoutMs` records the timeout.\n *\n * @param {string} url The request URL.\n * @param {number} after The timeout in milliseconds.\n */\n static timeout(url: string, after: number): HttpError {\n return new HttpError(0, `Request timed out after ${after} ms`, null, url, undefined, \"timeout\", undefined, after);\n }\n\n /**\n * An error for a 2xx response whose body could not be used: it failed to parse, a `transform`\n * threw, or there was no body where one was expected. `status` is the real status, `body` is\n * `null`, and `cause` carries the underlying error.\n *\n * @param {string} url The request URL.\n * @param {Response} response The response that could not be decoded.\n * @param {unknown} cause The parse error, the value a `transform` threw, or a description.\n */\n static decode(url: string, response: Response, cause: unknown): HttpError {\n return new HttpError(\n response.status,\n HttpErrorMessage.describe(cause),\n null,\n url,\n responseHeaders(response),\n \"decode\",\n cause\n );\n }\n\n /**\n * An error for a request that could not be built, so nothing was sent — typically a body that\n * could not be serialized. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} cause The underlying exception (typically a `TypeError` from `JSON.stringify`).\n */\n static request(url: string, cause: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"request\", cause);\n }\n\n /**\n * An error for a request aborted by something other than IO cancellation, such as an\n * interceptor aborting the signal. `status` is `0` and `body` is `null`.\n *\n * @param {string} url The request URL.\n * @param {unknown} [cause] The abort reason, if any.\n */\n static aborted(url: string, cause?: unknown): HttpError {\n return new HttpError(0, HttpErrorMessage.describe(cause), null, url, undefined, \"aborted\", cause);\n }\n\n /**\n * Narrows the `unknown` error body to a concrete type using a runtime type guard.\n * Returns `Some(body)` if the guard passes, `None` otherwise — so the narrow is *checked*,\n * not asserted. This keeps the unsafe cast out of consumer code: the body stays `unknown`\n * by default (forcing a conscious narrow), and this is the safe way to perform it.\n *\n * @template T The expected body shape.\n * @param {(x: unknown) => x is T} guard A type guard validating the body's shape.\n * @returns {Option<T>} `Some(body)` if the guard matches, `None` otherwise.\n *\n * @example\n * interface ApiError { code: string; message: string }\n * const isApiError = (x: unknown): x is ApiError =>\n * typeof x === \"object\" && x !== null && \"code\" in x && \"message\" in x;\n *\n * httpError.bodyAs(isApiError).fold(\n * () => \"unrecognized error shape\",\n * (e) => `${e.code}: ${e.message}`\n * );\n */\n bodyAs<T>(guard: (x: unknown) => x is T): Option<NonNullable<T>> {\n return guard(this.body) ? Option.ofNullable(this.body) : Option.empty();\n }\n}\n","import { Credentials, Method, Options } from \"./options\";\n\n/** @internal */\nexport interface RequestDefaults {\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n}\n\n/** @internal */\nexport interface PreparedRequest {\n readonly method: Method;\n readonly headers: Record<string, string>;\n readonly credentials: Credentials;\n readonly body: BodyInit | undefined;\n}\n\n/** Bodies `fetch` sends as-is: never JSON-encoded, never given an automatic content type. */\nconst isNativeBody = (body: unknown): boolean =>\n typeof body !== \"object\" ||\n body === null ||\n body instanceof FormData ||\n body instanceof Blob ||\n body instanceof ArrayBuffer ||\n body instanceof URLSearchParams ||\n (typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream) ||\n ArrayBuffer.isView(body);\n\nconst normalizeHeaders = (headers: Record<string, string | undefined>): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([key, value]) => [key.toLowerCase(), value])\n );\n\n/** `FormData` must carry no content type so the browser can set the multipart boundary. */\nconst withContentType = (headers: Record<string, string>, body: unknown): Record<string, string> => {\n if (body instanceof FormData) {\n const { \"content-type\": _dropped, ...rest } = headers;\n return rest;\n }\n const autoJson = body != null && typeof body === \"object\" && !isNativeBody(body) && !(\"content-type\" in headers);\n return autoJson ? { ...headers, \"content-type\": \"application/json\" } : headers;\n};\n\n/**\n * `application/json`, with or without parameters (`; charset=utf-8`), or any `+json` suffix type\n * (`application/merge-patch+json`, `application/vnd.api+json`). Matching the exact string alone\n * would send an object declared as one of the others as `\"[object Object]\"`.\n */\nconst isJsonContentType = (contentType: string): boolean => {\n const media = contentType.split(\";\")[0].trim().toLowerCase();\n return media === \"application/json\" || media.endsWith(\"+json\");\n};\n\nconst serializeBody = (method: Method, body: unknown, headers: Record<string, string>): BodyInit | undefined =>\n method === \"GET\" || method === \"HEAD\" || body == null\n ? undefined\n : isNativeBody(body)\n ? (body as BodyInit)\n : isJsonContentType(headers[\"content-type\"])\n ? JSON.stringify(body)\n : (body as BodyInit);\n\n/** @internal */\nexport const HttpRequest = {\n isNativeBody,\n normalizeHeaders,\n\n /** Per-request headers win over defaults; names are lowercased; the body follows the content type. */\n prepare: (method: Method, options: Options<unknown>, defaults: RequestDefaults): PreparedRequest => {\n const headers = withContentType(\n normalizeHeaders({ ...defaults.headers, ...(options.headers ?? {}) }),\n options.body\n );\n return {\n method,\n headers,\n credentials: options.credentials ?? defaults.credentials,\n body: serializeBody(method, options.body, headers),\n };\n },\n};\n","import { IO, Option } from \"@openstage/monadyssey-core\";\nimport { HttpError, responseHeaders } from \"./http-error\";\nimport { Method, ResponseType } from \"./options\";\n\ntype BodyReader = (response: Response) => Promise<unknown>;\n\nconst bodyReaders = {\n json: (response) => response.json(),\n text: (response) => response.text(),\n blob: (response) => response.blob(),\n arrayBuffer: (response) => response.arrayBuffer(),\n formData: (response) => response.formData(),\n} satisfies Record<ResponseType, BodyReader>;\n\n/** An own-property lookup, so an untyped `responseType` such as `\"toString\"` is rejected. */\nconst readerFor = (responseType: ResponseType): Option<BodyReader> =>\n Object.prototype.hasOwnProperty.call(bodyReaders, responseType)\n ? Option.of(bodyReaders[responseType])\n : Option.empty<BodyReader>();\n\nconst headers = responseHeaders;\n\n/** The server answered 2xx but the body could not be used (`kind: \"decode\"`). */\nconst error = (cause: unknown, url: string, response: Response): HttpError => HttpError.decode(url, response, cause);\n\n/**\n * What {@link HttpResponse.read} yields when there is no body at all — distinct from a JSON body\n * that is literally `null`.\n *\n * @internal\n */\nexport const NO_BODY: unique symbol = Symbol(\"HttpResponse.noBody\");\n\n/** The server answered with a non-2xx status; the parsed body travels with the error. */\nconst failure = (response: Response, body: unknown, url: string): HttpError =>\n new HttpError(response.status, response.statusText, body, url, headers(response));\n\n/**\n * Reads a JSON body, yielding `NO_BODY` for an empty one. Responses that implement only `json()` —\n * hand-rolled mocks in consumers' tests — are read with it.\n */\nconst readJson = async (response: Response): Promise<unknown> => {\n if (typeof response.text !== \"function\") return response.json();\n const text = await response.text();\n return text.trim() === \"\" ? NO_BODY : JSON.parse(text);\n};\n\n/** @internal */\nexport const HttpResponse = {\n headers,\n error,\n failure,\n\n /**\n * Reads a successful body. Yields `NO_BODY` when there is none to read: `204`/`205`, a `HEAD`\n * request, or an empty body under `\"json\"` (e.g. `201 Created` / `202 Accepted` with nothing\n * attached). A body that cannot be parsed fails with `kind: \"decode\"`.\n */\n read: (\n response: Response,\n responseType: ResponseType,\n url: string,\n method?: Method\n ): IO<HttpError, unknown | typeof NO_BODY> =>\n IO.from(readerFor(responseType), () => error(`Unsupported response type: ${responseType}`, url, response)).flatMap(\n (read) =>\n IO.lift(\n async () => {\n if (response.status === 204 || response.status === 205 || method === \"HEAD\") return NO_BODY;\n if (responseType !== \"json\") return read(response);\n return readJson(response);\n },\n (cause) => error(cause, url, response)\n )\n ),\n\n /**\n * Fails with the error for a non-2xx response. The body is read leniently, so an error page\n * that is not JSON (an HTML 502 from a proxy, say) still travels with the error and the message\n * stays the status text: under `\"json\"` the text is parsed when it is JSON and kept as a string\n * otherwise; an unreadable or empty body is `null`.\n */\n readFailure: (response: Response, responseType: ResponseType, url: string): IO<HttpError, never> =>\n IO.promise<unknown>(async () => {\n try {\n if (responseType !== \"json\") {\n return await readerFor(responseType).fold(\n async () => null,\n (read) => read(response)\n );\n }\n // A response without `text()` (a hand-rolled mock) can only be read as JSON.\n if (typeof response.text !== \"function\") return await response.json();\n const text = await response.text();\n if (text.trim() === \"\") return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n } catch {\n return null;\n }\n }).flatMap((body) => IO.fail(failure(response, body, url))),\n};\n","import { HttpError } from \"./http-error\";\n\nconst isAbort = (e: unknown): boolean =>\n typeof e === \"object\" && e !== null && \"name\" in e && (e as { name: unknown }).name === \"AbortError\";\n\n/** @internal */\nexport const HttpTransport = {\n /** An interceptor may reject with a ready-made `HttpError`; it is passed through untouched. */\n error: (e: unknown, url: string): HttpError =>\n e instanceof HttpError ? e : isAbort(e) ? HttpError.aborted(url, e) : HttpError.network(url, e),\n\n /** A timeout means the caller stopped waiting and an abort means someone wanted it stopped: neither is retried. */\n isRetryable: (error: HttpError): boolean =>\n error.kind === \"network\" || (error.kind === \"response\" && error.status >= 500),\n};\n","import { HttpError } from \"./http-error\";\nimport { HttpInterceptor, InterceptedRequest } from \"./options\";\n\n/** @internal */\nexport type Send = (request: InterceptedRequest) => Promise<Response>;\n\n/** @internal */\nexport const Interceptors = {\n /** A right fold, so the first registered interceptor is outermost. */\n run: (interceptors: readonly HttpInterceptor[], request: InterceptedRequest, send: Send): Promise<Response> =>\n interceptors.reduceRight<Send>((next, interceptor) => (r) => interceptor.intercept(r, next), send)(request),\n\n /**\n * Builds the request interceptors see: headers normalised to a `Headers` instance. A header the\n * platform rejects (a CR, LF or NUL in a value, say) is a `kind: \"request\"` error: the request could\n * not be built and nothing was sent, so it is not retried as a network failure.\n */\n request: (\n url: string,\n init: Omit<RequestInit, \"headers\"> & { headers: Record<string, string> }\n ): InterceptedRequest => {\n let headers: Headers;\n try {\n headers = new Headers(init.headers);\n } catch (e) {\n throw HttpError.request(url, e);\n }\n return { url, init: { ...init, headers } };\n },\n\n /** The end of the chain. */\n fetch: (request: InterceptedRequest): Promise<Response> => fetch(request.url, request.init),\n};\n","import { List, Stream } from \"@openstage/monadyssey-core\";\nimport type { Pipe } from \"@openstage/monadyssey-core\";\n\n/**\n * A single server-sent event parsed from a text/event-stream response.\n *\n * @property id - The last event id in effect when the event was dispatched — set by the event's\n * own `id:` field or by an earlier one, as the spec's `MessageEvent.lastEventId`. Absent until\n * the server sends an `id:`; the empty string after the server resets it. Used to resume a stream.\n * @property event - The event type. Defaults to `\"message\"` when the server sends no `event:` field.\n * @property data - The event payload. Multiple `data:` lines are joined with a newline.\n * @property retry - The reconnection time in milliseconds, if the server sent a `retry:` field.\n */\nexport interface ServerSentEvent {\n readonly id?: string;\n readonly event: string;\n readonly data: string;\n readonly retry?: number;\n}\n\n/**\n * The effects of one block (the lines up to a blank line). A block may set the last event id or\n * the reconnection time without dispatching an event — e.g. a lone `retry: 5000` or `id: 42`.\n *\n * @internal\n */\nexport interface SseFrame {\n /** The event to dispatch, or `null` when the block has no `data:` field. */\n readonly event: ServerSentEvent | null;\n /** The last `id:` value in the block, if any. The empty string resets the last event id. */\n readonly id?: string;\n /** The last valid `retry:` value in the block, if any. */\n readonly retry?: number;\n}\n\nconst parseBlock = (block: string): SseFrame => {\n let event = \"\";\n let id: string | undefined;\n let retry: number | undefined;\n const data: string[] = [];\n\n for (const line of block.split(\"\\n\")) {\n if (line === \"\" || line.startsWith(\":\")) continue; // blank line or comment\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n\n switch (field) {\n case \"event\":\n event = value;\n break;\n case \"data\":\n data.push(value);\n break;\n case \"id\":\n // Per the spec, an id containing NUL is ignored.\n if (!value.includes(\"\\0\")) id = value;\n break;\n case \"retry\":\n if (/^\\d+$/.test(value)) retry = Number.parseInt(value, 10);\n break;\n default:\n break;\n }\n }\n\n return {\n event:\n data.length === 0\n ? null\n : {\n // An empty `event:` field means the default type, not the empty string.\n event: event || \"message\",\n data: data.join(\"\\n\"),\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n },\n ...(id !== undefined ? { id } : {}),\n ...(retry !== undefined ? { retry } : {}),\n };\n};\n\n/**\n * `event` with `id` set to the last event id in effect (the spec's `lastEventId`), which persists\n * across events until the server changes it.\n *\n * @internal\n */\nexport const withLastEventId = (event: ServerSentEvent, lastEventId: string | undefined): ServerSentEvent => {\n const { id: _blockId, ...rest } = event;\n return lastEventId === undefined ? rest : { ...rest, id: lastEventId };\n};\n\n/**\n * Reassembles a byte stream into complete blocks; partial input and split multi-byte characters\n * are buffered. Call `reset` when a new connection begins. With a `limit`, an unterminated block\n * longer than that many characters throws `RangeError` (a hostile or broken server must not make the\n * buffer grow without bound); by default there is no limit.\n *\n * @internal\n */\nexport class FrameBuffer {\n private decoder = new TextDecoder(\"utf-8\");\n private buffered = \"\";\n\n constructor(private readonly limit: number = Infinity) {}\n /** The previous chunk ended in CR: a LF opening the next chunk belongs to that CRLF. */\n private skipLeadingLF = false;\n\n push(chunk: Uint8Array): SseFrame[] {\n let text = this.decoder.decode(chunk, { stream: true });\n if (text.length === 0) return [];\n if (this.skipLeadingLF && text.startsWith(\"\\n\")) text = text.slice(1);\n // A trailing CR ends its line now — holding it back until the next chunk would delay every\n // event on CR-only streams by a chunk. If a LF follows, it is skipped above.\n this.skipLeadingLF = text.endsWith(\"\\r\");\n // A block ends at the first `\\n\\n`; one may straddle the old buffer and this chunk, so the search\n // starts one character back. Without a delimiter the text is only appended, so a long block costs\n // one scan of each chunk rather than of everything buffered so far.\n const searchFrom = Math.max(0, this.buffered.length - 1);\n text = this.buffered + text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n if (text.indexOf(\"\\n\\n\", searchFrom) === -1) {\n this.buffered = text;\n this.checkLimit();\n return [];\n }\n const blocks = text.split(\"\\n\\n\");\n this.buffered = blocks.pop() ?? \"\";\n this.checkLimit();\n return blocks.map(parseBlock);\n }\n\n reset(): void {\n this.decoder = new TextDecoder(\"utf-8\");\n this.buffered = \"\";\n this.skipLeadingLF = false;\n }\n\n private checkLimit(): void {\n if (this.buffered.length > this.limit) {\n throw new RangeError(`An event block exceeds the ${this.limit}-character limit without ending`);\n }\n }\n}\n\n/**\n * A stream transformation that decodes raw response bytes into {@link ServerSentEvent}s, buffering\n * partial input across chunks. Single-use: each application maintains its own decoding state.\n *\n * @typeParam E - The error type carried by the surrounding stream.\n * @returns A pipe from a byte stream to a stream of parsed events.\n */\nexport const decodeEvents = <E>(): Pipe<E, Uint8Array, ServerSentEvent> => {\n const frames = new FrameBuffer();\n return (bytes: Stream<E, Uint8Array>) =>\n bytes.mapAccumulate<string | undefined, ServerSentEvent>(undefined, (lastEventId, chunk) => {\n const events: ServerSentEvent[] = [];\n let current = lastEventId;\n for (const frame of frames.push(chunk)) {\n if (frame.id !== undefined) current = frame.id;\n if (frame.event !== null) events.push(withLastEventId(frame.event, current));\n }\n return [current, List.from(events)] as const;\n });\n};\n","import { IO, Option, Schedule, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest } from \"./http-request\";\nimport { HttpResponse } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { Credentials, HttpInterceptor } from \"./options\";\nimport { FrameBuffer, ServerSentEvent, SseFrame, withLastEventId } from \"./sse\";\n\n/** A server-sent `retry:` below this is raised to it, so a misbehaving server cannot force a tight loop. */\nconst MIN_SERVER_RETRY_MS = 100;\n\n/** Largest delay `setTimeout` honours; anything above it (or Infinity) fires after ~1ms. */\nconst MAX_DELAY_MS = 2_147_483_647;\n\n/**\n * The longest event block (in UTF-16 code units, about 1 MiB of ASCII) buffered while waiting for its\n * terminating blank line. A server that never sends one would otherwise grow the buffer without bound;\n * reaching the limit fails the stream with `kind: \"decode\"`, which is not retried.\n */\nconst MAX_BLOCK_CHARS = 1_048_576;\n\n/** The `Last-Event-ID` value is the id UTF-8 encoded, byte for byte, as the EventSource spec sends it. */\nconst utf8ByteString = (id: string): string =>\n Array.from(new TextEncoder().encode(id), (b) => String.fromCharCode(b)).join(\"\");\n\ntype Reader = ReadableStreamDefaultReader<Uint8Array>;\n\n/** A cancellable sleep. The abort listener is removed when the timer fires, so waits do not accumulate listeners. */\nconst delay = (ms: number): IO<never, void> =>\n IO.promise<void>(\n (signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(\n () => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n },\n Math.min(ms, MAX_DELAY_MS)\n );\n signal.addEventListener(\"abort\", onAbort, { once: true });\n })\n );\n\n/**\n * Carried across reconnections within one run. Owns the one `AbortController` every connection\n * of the run is opened with, linked once to the run's signal; a connection registers a single\n * listener on it and removes it when closed, so listeners stay bounded however long the run lasts.\n */\nclass SseSession {\n readonly controller = new AbortController();\n readonly frames = new FrameBuffer(MAX_BLOCK_CHARS);\n readonly pending: SseFrame[] = [];\n lastEventId: string | undefined = undefined;\n retryMs: number;\n /** The server's `retry:` value, once received; it takes precedence over a `Schedule`'s delays. */\n serverRetryMs: number | undefined = undefined;\n /** Consecutive reconnects since the last delivered event. */\n reconnects = 0;\n /** The status of the current connection, for an error raised while reading it. */\n status = 200;\n private connection: { readonly reader: Reader; readonly unlink: () => void } | null = null;\n private readonly unlinkParent: () => void;\n\n constructor(retryMs: number, parent: AbortSignal) {\n this.retryMs = retryMs;\n const onAbort = () => this.controller.abort(parent.reason);\n parent.addEventListener(\"abort\", onAbort, { once: true });\n this.unlinkParent = () => parent.removeEventListener(\"abort\", onAbort);\n }\n\n get reader(): Reader | null {\n return this.connection?.reader ?? null;\n }\n\n attach(reader: Reader, status: number): void {\n this.status = status;\n const onAbort = () => reader.cancel().catch(() => {});\n this.controller.signal.addEventListener(\"abort\", onAbort, { once: true });\n this.connection = { reader, unlink: () => this.controller.signal.removeEventListener(\"abort\", onAbort) };\n this.frames.reset();\n }\n\n closeConnection(): void {\n if (this.connection) {\n this.connection.unlink();\n this.connection.reader.cancel().catch(() => {});\n this.connection = null;\n }\n }\n\n close(): void {\n this.closeConnection();\n this.unlinkParent();\n this.controller.abort();\n }\n}\n\n/** @internal */\nexport interface SseConfig {\n readonly url: string;\n readonly headers: Record<string, string>;\n readonly defaultHeaders: Record<string, string>;\n readonly credentials: Credentials;\n readonly interceptors: readonly HttpInterceptor[];\n readonly reconnect: boolean | Schedule;\n readonly retry: number;\n}\n\nconst mediaType = (contentType: string): string => contentType.split(\";\")[0].trim().toLowerCase();\n\nconst stream = (config: SseConfig): Stream<HttpError, ServerSentEvent> => {\n const { url, headers, defaultHeaders, credentials, interceptors, reconnect, retry } = config;\n\n type Opened = { readonly reader: Reader; readonly status: number };\n\n /** The reader for a new connection, or `null` when the server answered 204 (\"stop reconnecting\"). */\n const open = (session: SseSession): IO<HttpError, Opened | null> => {\n const prepared = HttpRequest.prepare(\n \"GET\",\n {\n headers: {\n ...headers,\n accept: \"text/event-stream\",\n // An empty id is how a server resets it: the header is sent only for a non-empty id.\n ...(session.lastEventId ? { \"last-event-id\": utf8ByteString(session.lastEventId) } : {}),\n },\n credentials,\n },\n { headers: defaultHeaders, credentials }\n );\n return IO.lift<HttpError, Response>(\n () =>\n Interceptors.run(\n interceptors,\n // `no-store`, as EventSource requests: a cached copy of a stream must never be replayed.\n Interceptors.request(url, { ...prepared, cache: \"no-store\", signal: session.controller.signal }),\n Interceptors.fetch\n ),\n (e: unknown) => HttpTransport.error(e, url)\n )\n .flatMap((response) => (response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"text\", url)))\n .flatMap((response): IO<HttpError, Opened | null> => {\n if (response.status === 204) {\n void response.body?.cancel().catch(() => {});\n return IO.of(null);\n }\n // Not an event stream (e.g. an HTML login page or SPA fallback): fail without\n // reconnecting, instead of parsing nothing and reconnecting forever. A missing\n // content type is tolerated.\n const contentType = response.headers.get(\"content-type\");\n if (contentType !== null && mediaType(contentType) !== \"text/event-stream\") {\n void response.body?.cancel().catch(() => {});\n return IO.fail(HttpResponse.error(`Expected text/event-stream, got ${contentType}`, url, response));\n }\n return response.body === null\n ? IO.fail<HttpError, Opened>(HttpResponse.error(\"Response has no body\", url, response))\n : IO.of({ reader: response.body.getReader(), status: response.status });\n });\n };\n\n const readChunk = (reader: Reader): IO<HttpError, ReadableStreamReadResult<Uint8Array>> =>\n IO.lift(\n () => reader.read(),\n (e: unknown) => HttpTransport.error(e, url)\n );\n\n type EventStep = Option<[ServerSentEvent, SseSession]>;\n const done = IO.of<EventStep>(Option.empty());\n\n /**\n * Reconnects after the scheduled delay, or gives up (`giveUp`) when reconnection is off or the\n * `Schedule` allows no further attempt. A server `retry:` value, once received, is the delay.\n */\n const reconnectOr = (session: SseSession, giveUp: IO<HttpError, EventStep>): IO<HttpError, EventStep> => {\n if (reconnect === false) return giveUp;\n const made = session.reconnects + 1;\n if (reconnect !== true && !reconnect.allowsAttempt(made)) return giveUp;\n const ms = session.serverRetryMs ?? (reconnect === true ? session.retryMs : reconnect.delayAfter(made));\n session.reconnects = made;\n return delay(ms).flatMap(() => step(session));\n };\n\n const step = (session: SseSession): IO<HttpError, EventStep> => {\n while (session.pending.length > 0) {\n const frame = session.pending.shift() as SseFrame;\n // `id:` and `retry:` take effect even in a block that dispatches no event.\n if (frame.id !== undefined) session.lastEventId = frame.id;\n if (frame.retry !== undefined) session.serverRetryMs = Math.max(frame.retry, MIN_SERVER_RETRY_MS);\n if (frame.event !== null) {\n session.reconnects = 0;\n return IO.of(\n Option.of<[ServerSentEvent, SseSession]>([withLastEventId(frame.event, session.lastEventId), session])\n );\n }\n }\n\n const reader = session.reader;\n if (reader === null) {\n return open(session).foldM<HttpError, EventStep>(\n (error) => (HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error)),\n (opened) => {\n if (opened === null) return done;\n session.attach(opened.reader, opened.status);\n return step(session);\n }\n );\n }\n\n return readChunk(reader).foldM<HttpError, EventStep>(\n (error) => {\n session.closeConnection();\n return HttpTransport.isRetryable(error) ? reconnectOr(session, IO.fail(error)) : IO.fail(error);\n },\n (result) => {\n if (result.done) {\n session.closeConnection();\n return reconnectOr(session, done);\n }\n let frames: SseFrame[];\n try {\n frames = session.frames.push(result.value);\n } catch (e) {\n // The block limit: the server is not sending an event stream we can read; stop for good.\n session.closeConnection();\n return IO.fail(\n new HttpError(session.status, String((e as Error).message), null, url, undefined, \"decode\", e)\n );\n }\n // One push per frame: spreading a chunk that holds hundreds of thousands of frames overflows the stack.\n for (const frame of frames) session.pending.push(frame);\n return step(session);\n }\n );\n };\n\n return Stream.bracket<HttpError, HttpError, SseSession, ServerSentEvent>(\n IO.promise(async (signal: AbortSignal) => new SseSession(retry, signal)),\n (session) => Stream.unfoldEval(session, step),\n (session) => IO.sync(() => session.close())\n );\n};\n\n/** @internal */\nexport const SseClient = { stream };\n","import { IO, Option, Stream } from \"@openstage/monadyssey-core\";\nimport { HttpError } from \"./http-error\";\nimport { HttpRequest, PreparedRequest } from \"./http-request\";\nimport { HttpResponse, NO_BODY } from \"./http-response\";\nimport { HttpTransport } from \"./http-transport\";\nimport { Interceptors } from \"./interceptors\";\nimport { SseClient } from \"./sse-client\";\nimport {\n Credentials,\n Exact,\n HttpClientConfig,\n HttpInterceptor,\n Method,\n Options,\n SendOptions,\n SseOptions,\n} from \"./options\";\nimport { ServerSentEvent } from \"./sse\";\n\n/** Options for a request that expects no body back, or `Option` of one. */\ntype OptionalBody<A> = Options<A> & { empty: \"option\" };\n/** Options for a request whose body is required (the default). */\ntype RequiredBody<A> = Options<A> & { empty?: \"error\" };\n\n/**\n * A composable HTTP client that wraps the native `fetch` API, returning `IO` instances instead of\n * Promises. Each instance carries its own configuration (base URL, interceptors, default headers,\n * timeout, credentials), so different parts of an application can use independently configured\n * clients.\n *\n * Three kinds of call, by what you want back:\n * - **the decoded body** — `get`, `post`, `put`, `patch`, `delete`, `fetch` return\n * `IO<HttpError, A>`. A response with no body (`204`, an empty `200`) fails with\n * `kind: \"decode\"`, since a value was expected; pass `{ empty: \"option\" }` when absence is\n * legitimate, to get `IO<HttpError, Option<A>>`.\n * - **nothing** — `send(method, uri, { body })` returns `IO<HttpError, void>` for actions whose\n * response body you do not read (publish, archive, leave …); any body, or none, is fine.\n * - **the `Response`** — `raw(method, uri, options?)` returns `IO<HttpError, Response>` for\n * status codes, headers or streaming.\n *\n * Every non-2xx response fails with `kind: \"response\"`, whichever you use. Cancellation is\n * supported: when the IO is cancelled (via a fiber or `timeout`), the underlying `fetch` is\n * aborted through its `AbortSignal`.\n *\n * `A` is what you claim the body is; the JSON is not checked unless you pass a `transform` that\n * validates it (a schema's `parse`, say) — a throw there fails with `kind: \"decode\"`.\n *\n * **Default credentials:** `same-origin`, matching the platform `fetch` default. Set\n * `credentials: \"include\"` explicitly per-client or per-request if you need cookies to flow\n * cross-origin.\n *\n * @example\n * const api = new HttpClient({\n * baseUrl: \"https://api.example.com\",\n * interceptors: [authInterceptor],\n * timeout: 5000,\n * });\n *\n * const users = api.get<User[]>(\"/users\"); // IO<HttpError, User[]>\n * const avatar = api.get<Avatar>(\"/me/avatar\", { empty: \"option\" }); // IO<HttpError, Option<Avatar>>\n * const leave = api.send(\"POST\", `/communities/${slug}/leave`); // IO<HttpError, void>\n */\nexport class HttpClient {\n private readonly interceptors: readonly HttpInterceptor[];\n private readonly baseUrl: string | undefined;\n private readonly defaultHeaders: Record<string, string>;\n private readonly defaultTimeout: number | undefined;\n private readonly defaultCredentials: Credentials;\n\n /**\n * Creates a new HttpClient with the given configuration.\n *\n * @param {HttpClientConfig} config - Configuration for the client.\n */\n constructor(config: HttpClientConfig = {}) {\n this.interceptors = Object.freeze([...(config.interceptors ?? [])]);\n this.baseUrl = config.baseUrl;\n this.defaultHeaders = config.defaultHeaders ?? {};\n this.defaultTimeout = config.timeout;\n this.defaultCredentials = config.credentials ?? \"same-origin\";\n }\n\n /**\n * Performs a GET request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n get<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n get<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n get<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"GET\", options);\n }\n\n /**\n * Performs a POST request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n post<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n post<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n post<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"POST\", { ...options, body });\n }\n\n /**\n * Performs a PUT request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n put<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n put<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n put<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PUT\", { ...options, body });\n }\n\n /**\n * Performs a PATCH request and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {unknown} [body] - The request payload.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n patch<A = unknown>(uri: string, body: unknown, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n patch<A = unknown>(uri: string, body?: unknown, options?: RequiredBody<A>): IO<HttpError, A>;\n patch<A = unknown>(uri: string, body?: unknown, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"PATCH\", { ...options, body });\n }\n\n /**\n * Performs a DELETE request and decodes the body. For a DELETE that answers `204 No Content`,\n * use `send(\"DELETE\", uri)` instead.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Options<A>} [options] - Request options; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n delete<A = unknown>(uri: string, options: Omit<OptionalBody<A>, \"body\">): IO<HttpError, Option<NonNullable<A>>>;\n delete<A = unknown>(uri: string, options?: Omit<RequiredBody<A>, \"body\">): IO<HttpError, A>;\n delete<A = unknown>(uri: string, options: Omit<Options<A>, \"body\"> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, \"DELETE\", options);\n }\n\n /**\n * Performs a request with any method and decodes the body.\n *\n * @template A - The type of the decoded body.\n * @param {string} uri - The URL or path to request.\n * @param {Method} method - The HTTP method.\n * @param {Options<A>} [options] - Request options, including `body`; `{ empty: \"option\" }` yields `Option<A>`.\n * @returns {IO<HttpError, A>} The decoded body.\n */\n fetch<A = unknown>(uri: string, method: Method, options: OptionalBody<A>): IO<HttpError, Option<NonNullable<A>>>;\n fetch<A = unknown>(uri: string, method: Method, options?: RequiredBody<A>): IO<HttpError, A>;\n fetch<A = unknown>(uri: string, method: Method, options: Options<A> = {}): IO<HttpError, A | Option<NonNullable<A>>> {\n return this.request<A>(uri, method, options);\n }\n\n /**\n * Performs a request whose response body you do not need — an action such as publish,\n * archive or leave. Succeeds with `void` for any 2xx, with or without a body (which is\n * discarded); a non-2xx response fails with `kind: \"response\"` and its body.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body. Every key must be one of\n * `SendOptions`: a payload passed here by mistake (`send(\"POST\", url, article)`) is rejected at\n * compile time; it belongs in `{ body: article }`. A `GET` or `HEAD` request with a body throws\n * `TypeError` where it is built.\n * @returns {IO<HttpError, void>}\n *\n * @example\n * client.send(\"POST\", `/api/v1/communities/${slug}/leave`);\n * client.send(\"PUT\", `/api/v1/articles/${id}`, { body: article });\n * client.send(\"DELETE\", `/api/v1/passkeys/${id}`, { headers: { \"X-Step-Up\": token } });\n */\n send<O extends SendOptions>(method: Method, uri: string, options?: Exact<O, SendOptions>): IO<HttpError, void> {\n const url = this.resolveUrl(uri);\n const opts: SendOptions = options ?? {};\n return this.withTimeout(\n url,\n opts.timeout,\n this.exchange(url, method, opts).flatMap((response) =>\n response.ok\n ? IO.sync(() => void response.body?.cancel().catch(() => {}))\n : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Performs a request and yields the `Response` itself, for status codes, headers or streaming.\n * A non-2xx response still fails with `kind: \"response\"` (its body read into the error); a 2xx\n * `Response` is returned unread. When you only need the status or headers, cancel the body\n * (`response.body?.cancel()`) so the connection is released; `send` does this for you.\n *\n * @param {Method} method - The HTTP method.\n * @param {string} uri - The URL or path to request.\n * @param {SendOptions} [options] - Headers, credentials, timeout and body; every key must be one of\n * `SendOptions`, as for `send`.\n * @returns {IO<HttpError, Response>}\n *\n * @example\n * client.raw(\"POST\", \"/api/v1/communities\", { body: input }).map((r) => r.headers.get(\"Location\"));\n */\n raw<O extends SendOptions>(method: Method, uri: string, options?: Exact<O, SendOptions>): IO<HttpError, Response> {\n const url = this.resolveUrl(uri);\n const opts: SendOptions = options ?? {};\n return this.withTimeout(\n url,\n opts.timeout,\n this.exchange(url, method, opts).flatMap((response) =>\n response.ok ? IO.of(response) : HttpResponse.readFailure(response, \"json\", url)\n )\n );\n }\n\n /**\n * Opens a server-sent-events stream and emits each {@link ServerSentEvent} as it arrives.\n *\n * The request goes through the same pipeline as every other method, so headers (including\n * `Authorization`), credentials, interceptors, and the base URL all apply. No request timeout is\n * used — the connection is long-lived. Cancelling the terminal `IO` (via `fork`/`timeout`) aborts\n * the connection and releases resources.\n *\n * By default the stream reconnects automatically when the connection ends or fails with a\n * retryable error (network error or `5xx`), resuming with `Last-Event-ID` and re-applying headers\n * and credentials on each attempt; terminal `4xx` responses (including auth failures) end the\n * stream. See {@link SseOptions} to configure or disable this.\n *\n * @param {string} uri - The URL or path to connect to.\n * @param {SseOptions} [options] - Headers, credentials, and reconnection settings.\n * @returns {Stream<HttpError, ServerSentEvent>} A stream of parsed events.\n * @throws {RangeError} If `options.retry` is negative or `NaN`.\n *\n * @example\n * const events = client.sse(\"/events\", { headers: { Authorization: `Bearer ${token}` } });\n * await events.runForEach((event) => IO.sync(() => handle(event))).unsafeRun();\n */\n sse(uri: string, options: SseOptions = {}): Stream<HttpError, ServerSentEvent> {\n // A negative or NaN delay is a programming error; left through, `setTimeout` would treat it\n // as 0 and reconnect in a tight loop.\n if (options.retry !== undefined && !(options.retry >= 0)) {\n throw new RangeError(`HttpClient.sse: retry must be a non-negative number of milliseconds, got ${options.retry}`);\n }\n return SseClient.stream({\n url: this.resolveUrl(uri),\n headers: options.headers ?? {},\n defaultHeaders: this.defaultHeaders,\n credentials: options.credentials ?? this.defaultCredentials,\n interceptors: this.interceptors,\n reconnect: options.reconnect ?? true,\n retry: options.retry ?? 3000,\n });\n }\n\n private resolveUrl(uri: string): string {\n // Absolute only with a scheme followed by `//` (or a data:/blob: URL). `new URL()` also\n // accepts `items:batchGet` or `localhost:3000` as absolute, which would bypass `baseUrl`.\n if (!this.baseUrl || /^[a-z][a-z\\d+.-]*:\\/\\//i.test(uri) || /^(data|blob):/i.test(uri)) return uri;\n const base = this.baseUrl.endsWith(\"/\") ? this.baseUrl.slice(0, -1) : this.baseUrl;\n const path = uri.startsWith(\"/\") ? uri : `/${uri}`;\n return `${base}${path}`;\n }\n\n /** Prepares the request, runs it through the interceptors and `fetch`, and yields the `Response`. */\n private exchange(url: string, method: Method, options: SendOptions): IO<HttpError, Response> {\n if ((method === \"GET\" || method === \"HEAD\") && options.body != null) {\n // A programming error, so it throws where the request is built instead of being dropped.\n throw new TypeError(`A ${method} request cannot have a body (${url})`);\n }\n const defaults = { headers: this.defaultHeaders, credentials: this.defaultCredentials };\n const prepare: IO<HttpError, PreparedRequest> = IO.lift(\n () => HttpRequest.prepare(method, options, defaults),\n (e: unknown) => HttpError.request(url, e)\n );\n return prepare.flatMap((prepared) =>\n IO.cancellable(\n (signal: AbortSignal) =>\n Interceptors.run(this.interceptors, Interceptors.request(url, { ...prepared, signal }), Interceptors.fetch),\n (e: unknown) => HttpTransport.error(e, url)\n )\n );\n }\n\n private withTimeout<A>(url: string, timeout: number | undefined, io: IO<HttpError, A>): IO<HttpError, A> {\n const ms = timeout ?? this.defaultTimeout;\n return ms == null ? io : io.timeout(ms, () => HttpError.timeout(url, ms));\n }\n\n private request<A>(uri: string, method: Method, options: Options<A>): IO<HttpError, A | Option<NonNullable<A>>> {\n const url = this.resolveUrl(uri);\n const { responseType = \"json\", transform = (data: unknown) => data as A, empty = \"error\" } = options;\n\n const decode = (response: Response): IO<HttpError, A | Option<NonNullable<A>>> =>\n response.ok\n ? HttpResponse.read(response, responseType, url, method).flatMap((parsed) => {\n if (parsed === NO_BODY) {\n return empty === \"option\"\n ? IO.of(Option.empty<NonNullable<A>>())\n : IO.fail(\n HttpError.decode(\n url,\n response,\n `expected a response body, got none (status ${response.status}); use send() or { empty: \"option\" }`\n )\n );\n }\n return IO.lift<HttpError, A>(\n () => transform(parsed),\n (e: unknown) => HttpError.decode(url, response, e)\n ).map((value) => (empty === \"option\" ? Option.ofNullable(value) : value));\n })\n : HttpResponse.readFailure(response, responseType, url);\n\n return this.withTimeout(url, options.timeout, this.exchange(url, method, options).flatMap(decode));\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openstage/monadyssey-fetch",
|
|
3
|
-
"version": "3.0.0-rc.
|
|
3
|
+
"version": "3.0.0-rc.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://gitlab.com/open-stage/monadyssey.git"
|
|
11
|
+
"url": "git+https://gitlab.com/open-stage/monadyssey.git"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"functional programming",
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"prettier:fix": "npm run prettier -- --write"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@openstage/monadyssey-core": "^3.0.0-rc.
|
|
46
|
+
"@openstage/monadyssey-core": "^3.0.0-rc.2"
|
|
47
47
|
}
|
|
48
48
|
}
|