@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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/internal/core.d.ts +115 -0
- package/build/internal/core.d.ts.map +1 -0
- package/build/internal/core.js +119 -0
- package/build/internal/decoders.d.ts +107 -0
- package/build/internal/decoders.d.ts.map +1 -0
- package/build/internal/decoders.js +110 -0
- package/build/internal/either.d.ts +25 -0
- package/build/internal/either.d.ts.map +1 -0
- package/build/internal/either.js +28 -0
- package/build/internal/interceptors.d.ts +101 -0
- package/build/internal/interceptors.d.ts.map +1 -0
- package/build/internal/interceptors.js +219 -0
- package/build/internal/statuses/statuses.d.ts +7 -0
- package/build/internal/statuses/statuses.d.ts.map +1 -0
- package/build/internal/statuses/statuses.js +11 -0
- package/build/internal/statuses/validators.d.ts +59 -0
- package/build/internal/statuses/validators.d.ts.map +1 -0
- package/build/internal/statuses/validators.js +78 -0
- package/build/internal/types.d.ts +3 -0
- package/build/internal/types.d.ts.map +1 -0
- package/build/internal/types.js +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +69 -0
|
@@ -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 @@
|
|
|
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 {};
|