unfee 0.1.2 → 1.0.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/README.md +1 -0
- package/dist/index.d.ts +35 -10
- package/dist/index.js +121 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# unfee
|
package/dist/index.d.ts
CHANGED
|
@@ -8,30 +8,41 @@ declare class HTTPError extends Error {
|
|
|
8
8
|
constructor(response: Response, request: Request, options: FetchOptions);
|
|
9
9
|
}
|
|
10
10
|
//#endregion
|
|
11
|
+
//#region src/parse-error.d.ts
|
|
12
|
+
declare class ParseError extends Error {
|
|
13
|
+
readonly response: Response;
|
|
14
|
+
readonly request: Request;
|
|
15
|
+
readonly options: FetchOptions;
|
|
16
|
+
constructor(response: Response, request: Request, options: FetchOptions, cause?: unknown);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
11
19
|
//#region src/types.d.ts
|
|
12
20
|
type Input = string | URL;
|
|
13
21
|
interface Fetch {
|
|
14
22
|
<T = any>(input: Input, options?: FetchOptions): Promise<FetchResponse<T>>;
|
|
15
23
|
extend: (options: ExtendOptions) => Fetch;
|
|
16
24
|
}
|
|
17
|
-
|
|
18
|
-
headers: Headers;
|
|
19
|
-
data: T;
|
|
20
|
-
status: number;
|
|
21
|
-
statusText: string;
|
|
22
|
-
}
|
|
25
|
+
type FetchResponse<T> = [error: null, data: T, response: Response | null] | [error: HTTPError | ParseError | Error, data: null, response: Response | null];
|
|
23
26
|
interface ExtendOptions {
|
|
24
27
|
baseUrl?: string;
|
|
25
28
|
headers?: HeadersInit;
|
|
26
29
|
query?: Record<string, any>;
|
|
27
30
|
hooks?: Hooks;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
retry?: Retry;
|
|
28
34
|
}
|
|
29
35
|
interface FetchOptions {
|
|
30
36
|
method?: RequestHttpVerbs;
|
|
31
37
|
query?: Record<string, any>;
|
|
32
38
|
headers?: HeadersInit;
|
|
33
39
|
data?: string | FormData | URLSearchParams | object;
|
|
40
|
+
responseType?: keyof typeof ResponseType;
|
|
34
41
|
hooks?: Hooks;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
native?: Omit<RequestInit, "method" | "headers" | "body" | "signal">;
|
|
45
|
+
retry?: Retry;
|
|
35
46
|
}
|
|
36
47
|
interface Hooks {
|
|
37
48
|
beforeRequest?: (options: Omit<FetchOptions, "hooks">) => void;
|
|
@@ -39,29 +50,43 @@ interface Hooks {
|
|
|
39
50
|
onRequestError?: (error: unknown, request: Request, options: Readonly<FetchOptions>) => void;
|
|
40
51
|
onResponseError?: (error: HTTPError, response: Response, request: Request, options: Readonly<FetchOptions>) => void;
|
|
41
52
|
onResponseParseError?: (error: unknown, response: Response, request: Request, options: Readonly<FetchOptions>) => void;
|
|
53
|
+
onRequestRetry?: (retryCount: number, response: Response, request: Request, options: Readonly<FetchOptions>) => void;
|
|
54
|
+
}
|
|
55
|
+
interface Retry {
|
|
56
|
+
times?: number;
|
|
57
|
+
delay?: number;
|
|
58
|
+
statusCode?: Set<number>;
|
|
42
59
|
}
|
|
43
60
|
interface Context {
|
|
44
|
-
method:
|
|
61
|
+
method: RequestHttpVerbs;
|
|
45
62
|
url: string;
|
|
46
63
|
headers: HeadersInit;
|
|
47
64
|
query: URLSearchParams;
|
|
48
65
|
data: BodyInit | undefined;
|
|
49
66
|
options: FetchOptions;
|
|
67
|
+
responseType?: ResponseType;
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
timeout?: number;
|
|
50
70
|
hooks: {
|
|
51
71
|
beforeRequest: Hooks["beforeRequest"][];
|
|
52
72
|
afterResponse: Hooks["afterResponse"][];
|
|
53
73
|
onRequestError: Hooks["onRequestError"][];
|
|
54
74
|
onResponseError: Hooks["onResponseError"][];
|
|
55
75
|
onResponseParseError: Hooks["onResponseParseError"][];
|
|
76
|
+
onRequestRetry: Hooks["onRequestRetry"][];
|
|
56
77
|
};
|
|
78
|
+
retry: Required<Retry>;
|
|
57
79
|
}
|
|
58
80
|
type RequestHttpVerbs = "get" | "post" | "put" | "patch" | "head" | "delete" | "options" | "trace";
|
|
59
81
|
declare enum ResponseType {
|
|
60
82
|
text = 0,
|
|
61
|
-
json = 1
|
|
83
|
+
json = 1,
|
|
84
|
+
formdata = 2,
|
|
85
|
+
arraybuffer = 3,
|
|
86
|
+
blob = 4
|
|
62
87
|
}
|
|
63
88
|
//#endregion
|
|
64
89
|
//#region src/index.d.ts
|
|
65
|
-
declare const
|
|
90
|
+
declare const unfee: Fetch;
|
|
66
91
|
//#endregion
|
|
67
|
-
export { Context, ExtendOptions, Fetch, FetchOptions, FetchResponse, Hooks, Input, RequestHttpVerbs, ResponseType,
|
|
92
|
+
export { Context, ExtendOptions, Fetch, FetchOptions, FetchResponse, Hooks, Input, RequestHttpVerbs, ResponseType, Retry, unfee };
|
package/dist/index.js
CHANGED
|
@@ -13,11 +13,40 @@ var HTTPError = class extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/constants.ts
|
|
18
|
+
const RETRY_STATUS_CODES = new Set([
|
|
19
|
+
408,
|
|
20
|
+
409,
|
|
21
|
+
425,
|
|
22
|
+
429,
|
|
23
|
+
500,
|
|
24
|
+
502,
|
|
25
|
+
503,
|
|
26
|
+
504
|
|
27
|
+
]);
|
|
28
|
+
const PAYLOAD_METHODS = new Set([
|
|
29
|
+
"post",
|
|
30
|
+
"put",
|
|
31
|
+
"patch",
|
|
32
|
+
"delete"
|
|
33
|
+
]);
|
|
34
|
+
const TEXT_TYPES = new Set([
|
|
35
|
+
"image/svg",
|
|
36
|
+
"application/xml",
|
|
37
|
+
"application/xhtml",
|
|
38
|
+
"application/html"
|
|
39
|
+
]);
|
|
40
|
+
const JSON_RESPONSE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(?:;.+)?$/i;
|
|
41
|
+
|
|
16
42
|
//#endregion
|
|
17
43
|
//#region src/types.ts
|
|
18
44
|
let ResponseType = /* @__PURE__ */ function(ResponseType) {
|
|
19
45
|
ResponseType[ResponseType["text"] = 0] = "text";
|
|
20
46
|
ResponseType[ResponseType["json"] = 1] = "json";
|
|
47
|
+
ResponseType[ResponseType["formdata"] = 2] = "formdata";
|
|
48
|
+
ResponseType[ResponseType["arraybuffer"] = 3] = "arraybuffer";
|
|
49
|
+
ResponseType[ResponseType["blob"] = 4] = "blob";
|
|
21
50
|
return ResponseType;
|
|
22
51
|
}({});
|
|
23
52
|
|
|
@@ -28,14 +57,18 @@ function constructRequest(ctx) {
|
|
|
28
57
|
return new Request(ctx.url + (query ? `?${query}` : ""), {
|
|
29
58
|
method: ctx.method,
|
|
30
59
|
headers: ctx.headers,
|
|
31
|
-
|
|
60
|
+
signal: ctx.signal,
|
|
61
|
+
body: ctx.data,
|
|
62
|
+
...ctx.options.native
|
|
32
63
|
});
|
|
33
64
|
}
|
|
34
65
|
function detectResponseType(responseTypeHeader) {
|
|
35
|
-
const JSON_RESPONSE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(?:;.+)?$/i;
|
|
36
66
|
responseTypeHeader = responseTypeHeader.split(";").shift() || "";
|
|
37
67
|
if (JSON_RESPONSE.test(responseTypeHeader)) return ResponseType.json;
|
|
38
|
-
return ResponseType.text;
|
|
68
|
+
if (TEXT_TYPES.has(responseTypeHeader) || responseTypeHeader.startsWith("text/")) return ResponseType.text;
|
|
69
|
+
if (responseTypeHeader.includes("application/octet-stream")) return ResponseType.arraybuffer;
|
|
70
|
+
if (responseTypeHeader.includes("multipart/form-data")) return ResponseType.formdata;
|
|
71
|
+
return ResponseType.blob;
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
//#endregion
|
|
@@ -62,6 +95,27 @@ function mergeParams(...params) {
|
|
|
62
95
|
});
|
|
63
96
|
return result;
|
|
64
97
|
}
|
|
98
|
+
function mergeRetry(...params) {
|
|
99
|
+
const result = {
|
|
100
|
+
times: 1,
|
|
101
|
+
delay: 0,
|
|
102
|
+
statusCode: RETRY_STATUS_CODES
|
|
103
|
+
};
|
|
104
|
+
for (const param of params) {
|
|
105
|
+
if (param.times !== void 0) result.times = param.times;
|
|
106
|
+
if (param.delay !== void 0) result.delay = param.delay;
|
|
107
|
+
if (param.statusCode !== void 0) result.statusCode = param.statusCode;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function mergeSignals(...signals) {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
signals.forEach((signal) => {
|
|
114
|
+
if (signal) if (signal.aborted) controller.abort();
|
|
115
|
+
else signal.addEventListener("abort", () => controller.abort());
|
|
116
|
+
});
|
|
117
|
+
return controller.signal;
|
|
118
|
+
}
|
|
65
119
|
|
|
66
120
|
//#endregion
|
|
67
121
|
//#region src/parse-error.ts
|
|
@@ -75,13 +129,18 @@ var ParseError = class extends Error {
|
|
|
75
129
|
}
|
|
76
130
|
};
|
|
77
131
|
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/utils.ts
|
|
134
|
+
function isIdempotentRequest(method) {
|
|
135
|
+
return !PAYLOAD_METHODS.has(method);
|
|
136
|
+
}
|
|
137
|
+
|
|
78
138
|
//#endregion
|
|
79
139
|
//#region src/fetch.ts
|
|
80
|
-
function createContext(input, options, extendOption) {
|
|
81
|
-
|
|
140
|
+
function createContext(input, options, extendOption, timeoutSignal) {
|
|
141
|
+
const method = options.method ?? "get";
|
|
82
142
|
let data;
|
|
83
143
|
let headers = { Accept: "application/json, text/plain, */*" };
|
|
84
|
-
if (options.method) method = options.method;
|
|
85
144
|
if (options.data instanceof URLSearchParams) {
|
|
86
145
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
87
146
|
data = options.data.toString();
|
|
@@ -98,70 +157,93 @@ function createContext(input, options, extendOption) {
|
|
|
98
157
|
...options.headers ?? {}
|
|
99
158
|
};
|
|
100
159
|
return {
|
|
101
|
-
method
|
|
160
|
+
method,
|
|
102
161
|
url: mergeURL(input, extendOption.baseUrl),
|
|
103
162
|
query: mergeParams(extendOption.query ?? {}, options.query ?? {}),
|
|
163
|
+
retry: mergeRetry(extendOption.retry ?? {}, options.retry ?? {}),
|
|
164
|
+
responseType: options.responseType ? ResponseType[options.responseType] : void 0,
|
|
104
165
|
headers,
|
|
105
166
|
data,
|
|
106
167
|
options,
|
|
168
|
+
timeout: options.timeout ?? extendOption.timeout,
|
|
169
|
+
signal: mergeSignals(extendOption.signal, options.signal, timeoutSignal),
|
|
107
170
|
hooks: {
|
|
108
171
|
onRequestError: [extendOption.hooks?.onRequestError, options.hooks?.onRequestError],
|
|
109
172
|
onResponseError: [extendOption.hooks?.onResponseError, options.hooks?.onResponseError],
|
|
110
173
|
beforeRequest: [extendOption.hooks?.beforeRequest, options.hooks?.beforeRequest],
|
|
111
174
|
afterResponse: [extendOption.hooks?.afterResponse, options.hooks?.afterResponse],
|
|
112
|
-
onResponseParseError: [extendOption.hooks?.onResponseParseError, options.hooks?.onResponseParseError]
|
|
175
|
+
onResponseParseError: [extendOption.hooks?.onResponseParseError, options.hooks?.onResponseParseError],
|
|
176
|
+
onRequestRetry: [extendOption.hooks?.onRequestRetry, options.hooks?.onRequestRetry]
|
|
113
177
|
}
|
|
114
178
|
};
|
|
115
179
|
}
|
|
116
180
|
async function createFetchResponse(response, request, ctx) {
|
|
117
|
-
|
|
181
|
+
ctx.hooks.afterResponse[0]?.(request, response, ctx.options);
|
|
182
|
+
ctx.hooks.afterResponse[1]?.(request, response, ctx.options);
|
|
183
|
+
const contentType = ctx.responseType ?? detectResponseType(response.headers.get("content-type") ?? "application/json");
|
|
118
184
|
let data;
|
|
119
185
|
try {
|
|
120
186
|
if (contentType === ResponseType.json) data = await response.json();
|
|
187
|
+
else if (contentType === ResponseType.formdata) data = await response.formData();
|
|
188
|
+
else if (contentType === ResponseType.arraybuffer) data = await response.arrayBuffer();
|
|
189
|
+
else if (contentType === ResponseType.blob) data = await response.blob();
|
|
121
190
|
else data = await response.text();
|
|
122
191
|
} catch (error) {
|
|
123
192
|
throw new ParseError(response, request, ctx.options, error);
|
|
124
193
|
}
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
async function handleFetchError(response, request, ctx, error, retryCount) {
|
|
197
|
+
if (error instanceof ParseError) {
|
|
198
|
+
ctx.hooks.onResponseParseError[0]?.(error, response, request, ctx.options);
|
|
199
|
+
ctx.hooks.onResponseParseError[1]?.(error, response, request, ctx.options);
|
|
200
|
+
} else if (error instanceof HTTPError) {
|
|
201
|
+
ctx.hooks.onResponseError[0]?.(error, response, request, ctx.options);
|
|
202
|
+
ctx.hooks.onResponseError[1]?.(error, response, request, ctx.options);
|
|
203
|
+
if (isIdempotentRequest(ctx.method) && ctx.retry.statusCode.has(response.status) && retryCount < ctx.retry.times) {
|
|
204
|
+
ctx.hooks.onRequestRetry[0]?.(retryCount, response, request, ctx.options);
|
|
205
|
+
ctx.hooks.onRequestRetry[1]?.(retryCount, response, request, ctx.options);
|
|
206
|
+
if (ctx.retry.delay > 0) await new Promise((resolve) => setTimeout(resolve, ctx.retry.delay));
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
ctx.hooks.onRequestError[0]?.(error, request, ctx.options);
|
|
210
|
+
ctx.hooks.onRequestError[1]?.(error, request, ctx.options);
|
|
211
|
+
}
|
|
131
212
|
}
|
|
132
213
|
function createFetch(extendOptions) {
|
|
133
214
|
const $fetch = async function(input, options = {}) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
context.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
215
|
+
let context = null;
|
|
216
|
+
let maxRetries = 0;
|
|
217
|
+
for (let retryCount = 0; retryCount <= maxRetries; retryCount++) {
|
|
218
|
+
extendOptions.hooks?.beforeRequest?.(options);
|
|
219
|
+
options.hooks?.beforeRequest?.(options);
|
|
220
|
+
const timeoutController = new AbortController();
|
|
221
|
+
let timer;
|
|
222
|
+
context = createContext(input, options, extendOptions, timeoutController.signal);
|
|
223
|
+
if (context.timeout) timer = setTimeout(() => {
|
|
224
|
+
timeoutController.abort();
|
|
225
|
+
}, context.timeout);
|
|
226
|
+
maxRetries = context.retry.times;
|
|
227
|
+
const request = constructRequest(context);
|
|
228
|
+
let inflight;
|
|
229
|
+
try {
|
|
230
|
+
inflight = await fetch(request);
|
|
231
|
+
if (!inflight.ok) throw new HTTPError(inflight, request, context.options);
|
|
232
|
+
return await createFetchResponse(inflight, request, context);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
await handleFetchError(inflight, request, context, error, retryCount);
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timer);
|
|
155
237
|
}
|
|
156
|
-
throw error;
|
|
157
238
|
}
|
|
239
|
+
throw new Error("Failed to fetch after retries");
|
|
158
240
|
};
|
|
159
241
|
return Object.assign($fetch, { extend: createFetch });
|
|
160
242
|
}
|
|
161
243
|
|
|
162
244
|
//#endregion
|
|
163
245
|
//#region src/index.ts
|
|
164
|
-
const
|
|
246
|
+
const unfee = createFetch({});
|
|
165
247
|
|
|
166
248
|
//#endregion
|
|
167
|
-
export { ResponseType,
|
|
249
|
+
export { ResponseType, unfee };
|