@jamx/http 0.1.0

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.
@@ -0,0 +1,101 @@
1
+ import { Result, TimeoutError } from "./core.js";
2
+ import type { Left } from "./either.js";
3
+ /**
4
+ * Rebases a request URL onto a configured base URL while preserving the
5
+ * original path, query string, and hash.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { composeInterceptors, defaultFetch, withBaseUrl } from "@jamx/http";
10
+ *
11
+ * const fetcher = composeInterceptors(
12
+ * withBaseUrl("https://api.example.com/v1"),
13
+ * )(defaultFetch);
14
+ * ```
15
+ */
16
+ export declare const withBaseUrl: (baseUrl: string | URL) => ({ request, next }: import("./core.js").Chain) => Promise<Result>;
17
+ /**
18
+ * Merges additional headers into the outgoing request.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { composeInterceptors, defaultFetch, withHeaders } from "@jamx/http";
23
+ *
24
+ * const fetcher = composeInterceptors(
25
+ * withHeaders({ accept: "application/json" }),
26
+ * )(defaultFetch);
27
+ * ```
28
+ */
29
+ export declare const withHeaders: (headers: HeadersInit | ((request: Request) => HeadersInit)) => ({ request, next }: import("./core.js").Chain) => Promise<Result>;
30
+ /**
31
+ * Adds an `Authorization` header using the given token and scheme.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { composeInterceptors, defaultFetch, withAuth } from "@jamx/http";
36
+ *
37
+ * const fetcher = composeInterceptors(withAuth("demo-token"))(defaultFetch);
38
+ * ```
39
+ */
40
+ export declare const withAuth: (token: string | ((request: Request) => string), scheme?: string) => ({ request, next }: import("./core.js").Chain) => Promise<Result>;
41
+ /**
42
+ * Aborts requests that take longer than the given timeout.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { composeInterceptors, defaultFetch, withTimeout } from "@jamx/http";
47
+ *
48
+ * const fetcher = composeInterceptors(withTimeout(500))(defaultFetch);
49
+ * ```
50
+ */
51
+ export declare const withTimeout: (timeoutMs: number) => ({ request, next }: import("./core.js").Chain) => Promise<Result | Left<TimeoutError>>;
52
+ export interface RetryOptions {
53
+ retries: number;
54
+ methods?: readonly string[];
55
+ shouldRetry?: (result: Result, attempt: number, request: Request) => boolean | Promise<boolean>;
56
+ }
57
+ /**
58
+ * Retries idempotent requests when they fail with transport errors or `5xx`
59
+ * responses.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import { composeInterceptors, defaultFetch, withRetry } from "@jamx/http";
64
+ *
65
+ * const fetcher = composeInterceptors(withRetry({ retries: 2 }))(defaultFetch);
66
+ * ```
67
+ */
68
+ export declare const withRetry: (options: RetryOptions) => ({ request, next }: import("./core.js").Chain) => Promise<Result>;
69
+ export interface CacheStore {
70
+ get(key: string): Response | undefined | Promise<Response | undefined>;
71
+ set(key: string, response: Response): void | Promise<void>;
72
+ }
73
+ export interface CacheOptions {
74
+ store?: CacheStore;
75
+ key?: (request: Request) => string;
76
+ shouldCache?: (result: Result, request: Request) => boolean;
77
+ }
78
+ /**
79
+ * Caches successful `GET` responses in a store.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { composeInterceptors, createMemoryCacheStore, defaultFetch, withCache } from "@jamx/http";
84
+ *
85
+ * const store = createMemoryCacheStore();
86
+ * const fetcher = composeInterceptors(withCache({ store }))(defaultFetch);
87
+ * ```
88
+ */
89
+ export declare const withCache: (options?: CacheOptions) => ({ request, next }: import("./core.js").Chain) => Promise<Result>;
90
+ /**
91
+ * Creates an in-memory cache store compatible with `withCache`.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { createMemoryCacheStore } from "@jamx/http";
96
+ *
97
+ * const store = createMemoryCacheStore();
98
+ * ```
99
+ */
100
+ export declare const createMemoryCacheStore: () => CacheStore;
101
+ //# sourceMappingURL=interceptors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptors.d.ts","sourceRoot":"","sources":["../../src/internal/interceptors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,GAAG,GAAG,sEAW7C,CAAC;AAEL;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GACtB,SAAS,WAAW,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,WAAW,CAAC,sEAYxD,CAAC;AAEL;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GACnB,OAAO,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,EAC9C,eAAiB,sEAOf,CAAC;AAEL;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,WAAW,MAAM,2FA4BzC,CAAC;AA8BL,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,KACb,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,YAAY,sEAmB3C,CAAC;AAuBL,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACnC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;CAC7D;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,UAAS,YAAiB,sEA0BnD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,QAAO,UAYzC,CAAC"}
@@ -0,0 +1,219 @@
1
+ import { defineInterceptor, TimeoutError } from "./core.js";
2
+ import { isErr, isOk, ok } from "./either.js";
3
+ /**
4
+ * Rebases a request URL onto a configured base URL while preserving the
5
+ * original path, query string, and hash.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { composeInterceptors, defaultFetch, withBaseUrl } from "@jamx/http";
10
+ *
11
+ * const fetcher = composeInterceptors(
12
+ * withBaseUrl("https://api.example.com/v1"),
13
+ * )(defaultFetch);
14
+ * ```
15
+ */
16
+ export const withBaseUrl = (baseUrl) => defineInterceptor(async ({ request, next }) => {
17
+ const currentUrl = new URL(request.url);
18
+ const url = new URL(`${trimTrailingSlash(baseUrl.toString())}/${trimLeadingSlash(`${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`)}`);
19
+ return next(new Request(url, request));
20
+ });
21
+ /**
22
+ * Merges additional headers into the outgoing request.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { composeInterceptors, defaultFetch, withHeaders } from "@jamx/http";
27
+ *
28
+ * const fetcher = composeInterceptors(
29
+ * withHeaders({ accept: "application/json" }),
30
+ * )(defaultFetch);
31
+ * ```
32
+ */
33
+ export const withHeaders = (headers) => defineInterceptor(async ({ request, next }) => {
34
+ const nextHeaders = new Headers(request.headers);
35
+ const extraHeaders = typeof headers === "function" ? headers(request) : headers;
36
+ new Headers(extraHeaders).forEach((value, key) => {
37
+ nextHeaders.set(key, value);
38
+ });
39
+ return next(new Request(request, { headers: nextHeaders }));
40
+ });
41
+ /**
42
+ * Adds an `Authorization` header using the given token and scheme.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { composeInterceptors, defaultFetch, withAuth } from "@jamx/http";
47
+ *
48
+ * const fetcher = composeInterceptors(withAuth("demo-token"))(defaultFetch);
49
+ * ```
50
+ */
51
+ export const withAuth = (token, scheme = "Bearer") => defineInterceptor(async ({ request, next }) => {
52
+ const resolvedToken = typeof token === "function" ? token(request) : token;
53
+ const headers = new Headers(request.headers);
54
+ headers.set("authorization", `${scheme} ${resolvedToken}`);
55
+ return next(new Request(request, { headers }));
56
+ });
57
+ /**
58
+ * Aborts requests that take longer than the given timeout.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { composeInterceptors, defaultFetch, withTimeout } from "@jamx/http";
63
+ *
64
+ * const fetcher = composeInterceptors(withTimeout(500))(defaultFetch);
65
+ * ```
66
+ */
67
+ export const withTimeout = (timeoutMs) => defineInterceptor(async ({ request, next }) => {
68
+ const controller = new AbortController();
69
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
70
+ const timedRequest = new Request(request, {
71
+ signal: mergeSignals(request.signal, controller.signal),
72
+ });
73
+ try {
74
+ return await Promise.race([
75
+ next(timedRequest),
76
+ new Promise((resolve) => {
77
+ timedRequest.signal.addEventListener("abort", () => {
78
+ resolve({
79
+ ok: false,
80
+ error: new TimeoutError(timeoutMs),
81
+ });
82
+ }, { once: true });
83
+ }),
84
+ ]);
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ });
90
+ const mergeSignals = (left, right) => {
91
+ if (!left) {
92
+ return right;
93
+ }
94
+ const anySignal = AbortSignal;
95
+ if (typeof anySignal.any === "function") {
96
+ return anySignal.any([left, right]);
97
+ }
98
+ const controller = new AbortController();
99
+ const abort = () => controller.abort();
100
+ if (left.aborted || right.aborted) {
101
+ controller.abort();
102
+ return controller.signal;
103
+ }
104
+ left.addEventListener("abort", abort, { once: true });
105
+ right.addEventListener("abort", abort, { once: true });
106
+ return controller.signal;
107
+ };
108
+ /**
109
+ * Retries idempotent requests when they fail with transport errors or `5xx`
110
+ * responses.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { composeInterceptors, defaultFetch, withRetry } from "@jamx/http";
115
+ *
116
+ * const fetcher = composeInterceptors(withRetry({ retries: 2 }))(defaultFetch);
117
+ * ```
118
+ */
119
+ export const withRetry = (options) => defineInterceptor(async ({ request, next }) => {
120
+ for (let attempt = 0; attempt <= options.retries; attempt += 1) {
121
+ const attemptRequest = new Request(request);
122
+ const result = await next(attemptRequest);
123
+ const shouldRetry = await shouldRetryResult(result, attempt, request, options);
124
+ if (!shouldRetry) {
125
+ return result;
126
+ }
127
+ }
128
+ return next(new Request(request));
129
+ });
130
+ const shouldRetryResult = async (result, attempt, request, options) => {
131
+ if (attempt >= options.retries) {
132
+ return false;
133
+ }
134
+ if (!isRetryableMethod(request.method, options.methods)) {
135
+ return false;
136
+ }
137
+ if (options.shouldRetry) {
138
+ return options.shouldRetry(result, attempt, request);
139
+ }
140
+ return isErr(result) || (isOk(result) && result.value.status >= 500);
141
+ };
142
+ /**
143
+ * Caches successful `GET` responses in a store.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { composeInterceptors, createMemoryCacheStore, defaultFetch, withCache } from "@jamx/http";
148
+ *
149
+ * const store = createMemoryCacheStore();
150
+ * const fetcher = composeInterceptors(withCache({ store }))(defaultFetch);
151
+ * ```
152
+ */
153
+ export const withCache = (options = {}) => {
154
+ const store = options.store ?? createMemoryCacheStore();
155
+ return defineInterceptor(async ({ request, next }) => {
156
+ const key = (options.key ?? defaultCacheKey)(request);
157
+ if (request.method === "GET") {
158
+ const cached = await store.get(key);
159
+ if (cached) {
160
+ return ok(cached.clone());
161
+ }
162
+ }
163
+ const result = await next(new Request(request));
164
+ if (request.method === "GET" &&
165
+ isOk(result) &&
166
+ (options.shouldCache ?? defaultShouldCache)(result, request)) {
167
+ await store.set(key, result.value.clone());
168
+ }
169
+ return result;
170
+ });
171
+ };
172
+ /**
173
+ * Creates an in-memory cache store compatible with `withCache`.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * import { createMemoryCacheStore } from "@jamx/http";
178
+ *
179
+ * const store = createMemoryCacheStore();
180
+ * ```
181
+ */
182
+ export const createMemoryCacheStore = () => {
183
+ const store = new Map();
184
+ return {
185
+ get(key) {
186
+ const cached = store.get(key);
187
+ return cached?.clone();
188
+ },
189
+ set(key, response) {
190
+ store.set(key, response.clone());
191
+ },
192
+ };
193
+ };
194
+ const defaultShouldCache = (result) => isOk(result) && result.value.ok;
195
+ const defaultCacheKey = (request) => `${request.method}:${request.url}:${serializeHeaders(request.headers)}`;
196
+ const DEFAULT_RETRY_METHODS = new Set([
197
+ "DELETE",
198
+ "GET",
199
+ "HEAD",
200
+ "OPTIONS",
201
+ "PUT",
202
+ "TRACE",
203
+ ]);
204
+ const isRetryableMethod = (method, methods) => {
205
+ const normalizedMethod = method.toUpperCase();
206
+ if (!methods) {
207
+ return DEFAULT_RETRY_METHODS.has(normalizedMethod);
208
+ }
209
+ return methods.some((candidate) => candidate.toUpperCase() === normalizedMethod);
210
+ };
211
+ const serializeHeaders = (headers) => {
212
+ const serialized = [];
213
+ headers.forEach((value, key) => {
214
+ serialized.push(`${key}:${value}`);
215
+ });
216
+ return serialized.sort((left, right) => left.localeCompare(right)).join("|");
217
+ };
218
+ const trimLeadingSlash = (value) => value.replace(/^\/+/, "");
219
+ const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
@@ -0,0 +1,7 @@
1
+ export declare const Informational: readonly [100, 101, 102, 103];
2
+ export declare const OK: readonly [200, 201, 202, 203, 204, 205, 206, 207, 208, 226];
3
+ export declare const Redirect: readonly [300, 301, 302, 303, 304, 305, 306, 307, 308];
4
+ export declare const ClientError: readonly [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451];
5
+ export declare const ServerError: readonly [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511];
6
+ export declare const ErrorStatuses: readonly [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511];
7
+ //# sourceMappingURL=statuses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statuses.d.ts","sourceRoot":"","sources":["../../../src/internal/statuses/statuses.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,+BAAgC,CAAC;AAE3D,eAAO,MAAM,EAAE,6DAA8D,CAAC;AAE9E,eAAO,MAAM,QAAQ,wDAAyD,CAAC;AAE/E,eAAO,MAAM,WAAW,4JAGd,CAAC;AAEX,eAAO,MAAM,WAAW,kEAEd,CAAC;AAEX,eAAO,MAAM,aAAa,mNAA4C,CAAC"}
@@ -0,0 +1,11 @@
1
+ export const Informational = [100, 101, 102, 103];
2
+ export const OK = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226];
3
+ export const Redirect = [300, 301, 302, 303, 304, 305, 306, 307, 308];
4
+ export const ClientError = [
5
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414,
6
+ 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
7
+ ];
8
+ export const ServerError = [
9
+ 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
10
+ ];
11
+ export const ErrorStatuses = [...ClientError, ...ServerError];
@@ -0,0 +1,59 @@
1
+ import { Either } from "../either.js";
2
+ export declare class StatusError extends Error {
3
+ name: string;
4
+ readonly status: number;
5
+ readonly statusText: string;
6
+ readonly response: Response;
7
+ readonly expected: number | readonly number[] | string;
8
+ constructor(response: Response, expected: number | readonly number[] | string, message?: string);
9
+ }
10
+ /**
11
+ * Asserts that a response matches an expected status code or one of several
12
+ * exact status codes.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { expectStatus, ok } from "@jamx/http";
17
+ *
18
+ * const result = expectStatus(ok(new Response(null, { status: 200 })), 200);
19
+ * ```
20
+ */
21
+ export declare const expectStatus: <TError>(result: Either<TError, Response>, expected: number | readonly number[]) => Either<TError | StatusError, Response>;
22
+ /**
23
+ * Asserts that a response status satisfies a custom predicate.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { expectStatusRange, ok } from "@jamx/http";
28
+ *
29
+ * const result = expectStatusRange(
30
+ * ok(new Response(null, { status: 204 })),
31
+ * (status) => status >= 200 && status < 300,
32
+ * "a successful status (2xx)",
33
+ * );
34
+ * ```
35
+ */
36
+ export declare const expectStatusRange: <TError>(result: Either<TError, Response>, predicate: (status: number) => boolean, description?: string) => Either<TError | StatusError, Response>;
37
+ export declare const isInformationalStatus: (status: number) => boolean;
38
+ export declare const isOkStatus: (status: number) => boolean;
39
+ export declare const isRedirectStatus: (status: number) => boolean;
40
+ export declare const isClientErrorStatus: (status: number) => boolean;
41
+ export declare const isServerErrorStatus: (status: number) => boolean;
42
+ export declare const isErrorStatus: (status: number) => boolean;
43
+ export declare const expectInformationalStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
44
+ /**
45
+ * Asserts that a response has a successful `2xx` status.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { expectOKStatus, ok } from "@jamx/http";
50
+ *
51
+ * const result = expectOKStatus(ok(new Response(null, { status: 204 })));
52
+ * ```
53
+ */
54
+ export declare const expectOKStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
55
+ export declare const expectRedirectStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
56
+ export declare const expectClientErrorStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
57
+ export declare const expectServerErrorStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
58
+ export declare const expectErrorStatus: <TError>(result: Either<TError, Response>) => Either<TError | StatusError, Response>;
59
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../src/internal/statuses/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,cAAc,CAAC;AAElD,qBAAa,WAAY,SAAQ,KAAK;IACpC,IAAI,SAAiB;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;gBAGrD,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,MAAM,EAC7C,OAAO,SAA+C;CAQzD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,EACjC,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAChC,UAAU,MAAM,GAAG,SAAS,MAAM,EAAE,KACnC,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CAgBvC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,EACtC,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAChC,WAAW,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,EACtC,oBAAiC,KAChC,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CAYvC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,YACrB,CAAC;AAEhC,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,YAAkC,CAAC;AAE5E,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,YAChB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,YACnB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,YACnB,CAAC;AAEhC,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,YACgB,CAAC;AAE7D,eAAO,MAAM,yBAAyB,GAAI,MAAM,EAC9C,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CAKrC,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,EACnC,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CAC4B,CAAC;AAErE,eAAO,MAAM,oBAAoB,GAAI,MAAM,EACzC,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CACgC,CAAC;AAEzE,eAAO,MAAM,uBAAuB,GAAI,MAAM,EAC5C,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CACuC,CAAC;AAEhF,eAAO,MAAM,uBAAuB,GAAI,MAAM,EAC5C,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CACuC,CAAC;AAEhF,eAAO,MAAM,iBAAiB,GAAI,MAAM,EACtC,QAAQ,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAC/B,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,QAAQ,CACkC,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { err, isErr } from "../either.js";
2
+ export class StatusError extends Error {
3
+ constructor(response, expected, message = `Unexpected HTTP status ${response.status}.`) {
4
+ super(message);
5
+ this.name = "StatusError";
6
+ this.status = response.status;
7
+ this.statusText = response.statusText;
8
+ this.response = response;
9
+ this.expected = expected;
10
+ }
11
+ }
12
+ /**
13
+ * Asserts that a response matches an expected status code or one of several
14
+ * exact status codes.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { expectStatus, ok } from "@jamx/http";
19
+ *
20
+ * const result = expectStatus(ok(new Response(null, { status: 200 })), 200);
21
+ * ```
22
+ */
23
+ export const expectStatus = (result, expected) => {
24
+ if (isErr(result))
25
+ return result;
26
+ const matches = Array.isArray(expected)
27
+ ? expected.includes(result.value.status)
28
+ : result.value.status === expected;
29
+ return matches
30
+ ? result
31
+ : err(new StatusError(result.value, expected, `Expected status ${formatExpectedStatus(expected)} but received ${result.value.status}.`));
32
+ };
33
+ /**
34
+ * Asserts that a response status satisfies a custom predicate.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { expectStatusRange, ok } from "@jamx/http";
39
+ *
40
+ * const result = expectStatusRange(
41
+ * ok(new Response(null, { status: 204 })),
42
+ * (status) => status >= 200 && status < 300,
43
+ * "a successful status (2xx)",
44
+ * );
45
+ * ```
46
+ */
47
+ export const expectStatusRange = (result, predicate, description = "a matching status") => {
48
+ if (isErr(result))
49
+ return result;
50
+ return predicate(result.value.status)
51
+ ? result
52
+ : err(new StatusError(result.value, description, `Expected ${description} but received ${result.value.status}.`));
53
+ };
54
+ export const isInformationalStatus = (status) => status >= 100 && status < 200;
55
+ export const isOkStatus = (status) => status >= 200 && status < 300;
56
+ export const isRedirectStatus = (status) => status >= 300 && status < 400;
57
+ export const isClientErrorStatus = (status) => status >= 400 && status < 500;
58
+ export const isServerErrorStatus = (status) => status >= 500 && status < 600;
59
+ export const isErrorStatus = (status) => isClientErrorStatus(status) || isServerErrorStatus(status);
60
+ export const expectInformationalStatus = (result) => expectStatusRange(result, isInformationalStatus, "an informational status (1xx)");
61
+ /**
62
+ * Asserts that a response has a successful `2xx` status.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { expectOKStatus, ok } from "@jamx/http";
67
+ *
68
+ * const result = expectOKStatus(ok(new Response(null, { status: 204 })));
69
+ * ```
70
+ */
71
+ export const expectOKStatus = (result) => expectStatusRange(result, isOkStatus, "a successful status (2xx)");
72
+ export const expectRedirectStatus = (result) => expectStatusRange(result, isRedirectStatus, "a redirect status (3xx)");
73
+ export const expectClientErrorStatus = (result) => expectStatusRange(result, isClientErrorStatus, "a client error status (4xx)");
74
+ export const expectServerErrorStatus = (result) => expectStatusRange(result, isServerErrorStatus, "a server error status (5xx)");
75
+ export const expectErrorStatus = (result) => expectStatusRange(result, isErrorStatus, "an error status (4xx or 5xx)");
76
+ function formatExpectedStatus(expected) {
77
+ return Array.isArray(expected) ? expected.join(", ") : String(expected);
78
+ }
@@ -0,0 +1,3 @@
1
+ import { Either } from "./either.js";
2
+ export type AnyEither = Either<any, any>;
3
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/internal/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};