@fuman/fetch 0.0.1
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 +8 -0
- package/README.md +13 -0
- package/_types.d.ts +8 -0
- package/addons/_utils.cjs +54 -0
- package/addons/_utils.d.ts +3 -0
- package/addons/_utils.js +54 -0
- package/addons/bundle.cjs +18 -0
- package/addons/bundle.d.ts +7 -0
- package/addons/bundle.js +18 -0
- package/addons/form.cjs +23 -0
- package/addons/form.d.ts +22 -0
- package/addons/form.js +23 -0
- package/addons/index.d.ts +3 -0
- package/addons/multipart.cjs +35 -0
- package/addons/multipart.d.ts +22 -0
- package/addons/multipart.js +35 -0
- package/addons/parse/_types.d.ts +11 -0
- package/addons/parse/adapters/valibot.d.ts +8 -0
- package/addons/parse/adapters/yup.d.ts +13 -0
- package/addons/parse/adapters/zod.d.ts +6 -0
- package/addons/parse/addon.cjs +12 -0
- package/addons/parse/addon.d.ts +6 -0
- package/addons/parse/addon.js +12 -0
- package/addons/query.cjs +22 -0
- package/addons/query.d.ts +17 -0
- package/addons/query.js +22 -0
- package/addons/rate-limit.cjs +65 -0
- package/addons/rate-limit.d.ts +62 -0
- package/addons/rate-limit.js +65 -0
- package/addons/retry.cjs +74 -0
- package/addons/retry.d.ts +58 -0
- package/addons/retry.js +74 -0
- package/addons/timeout.cjs +54 -0
- package/addons/timeout.d.ts +25 -0
- package/addons/timeout.js +54 -0
- package/addons/tough-cookie.d.ts +7 -0
- package/addons/types.d.ts +30 -0
- package/default.cjs +18 -0
- package/default.d.ts +30 -0
- package/default.js +18 -0
- package/ffetch.cjs +200 -0
- package/ffetch.d.ts +101 -0
- package/ffetch.js +200 -0
- package/index.cjs +10 -0
- package/index.d.ts +4 -0
- package/index.js +10 -0
- package/package.json +89 -0
- package/tough.cjs +24 -0
- package/tough.d.ts +1 -0
- package/tough.js +24 -0
- package/valibot.cjs +16 -0
- package/valibot.d.ts +1 -0
- package/valibot.js +16 -0
- package/yup.cjs +18 -0
- package/yup.d.ts +1 -0
- package/yup.js +18 -0
- package/zod.cjs +15 -0
- package/zod.d.ts +1 -0
- package/zod.js +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright 2024 alina sireneva
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
8
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# `@fuman/fetch`
|
|
2
|
+
|
|
3
|
+
no-bullshit minimal wrapper around `window.fetch` that aims to improve dx
|
|
4
|
+
by adding commonly used features, while having low runtime overhead,
|
|
5
|
+
small bundle size and staying close to the web standards.
|
|
6
|
+
|
|
7
|
+
## features
|
|
8
|
+
- no more `(await fetch(url)).json()` - just `await ffetch(url).json()`
|
|
9
|
+
- sugar for common request body types (json, form, multipart)
|
|
10
|
+
- base url, base headers, etc.
|
|
11
|
+
- retries, timeouts, validation built in
|
|
12
|
+
- basic middlewares
|
|
13
|
+
- **type-safe** compatibility with popular parsing libraries (yup, zod, valibot, etc.)
|
package/_types.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Middleware } from '@fuman/utils';
|
|
2
|
+
import { FfetchAddon } from './addons/types.js';
|
|
3
|
+
export type FetchLike = (req: Request) => Promise<Response>;
|
|
4
|
+
export type FfetchMiddleware = Middleware<Request, Response>;
|
|
5
|
+
export type CombineAddons<ResponseMixins extends FfetchAddon<any, any>[], AccRequest = {}, AccResponse = {}> = ResponseMixins extends [FfetchAddon<infer RequestMixin, infer ResponseMixin>, ...infer Rest extends FfetchAddon<any, any>[]] ? CombineAddons<Rest, AccRequest & RequestMixin, AccResponse & ResponseMixin> : {
|
|
6
|
+
readonly request: AccRequest;
|
|
7
|
+
readonly response: AccResponse;
|
|
8
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function urlencode(query) {
|
|
4
|
+
const search = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(query)) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
for (let i = 0; i < value.length; i++) {
|
|
8
|
+
search.append(key, String(value[i]));
|
|
9
|
+
}
|
|
10
|
+
} else {
|
|
11
|
+
search.set(key, String(value));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return search;
|
|
15
|
+
}
|
|
16
|
+
function setHeader(options, key, value) {
|
|
17
|
+
if (!options.headers) {
|
|
18
|
+
if (value === null) return;
|
|
19
|
+
options.headers = { [key]: value };
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let { headers } = options;
|
|
23
|
+
if (Array.isArray(headers)) {
|
|
24
|
+
if (value === null) {
|
|
25
|
+
for (let i = 0; i < headers.length; i++) {
|
|
26
|
+
if (headers[i][0] === key) {
|
|
27
|
+
headers.splice(i, 1);
|
|
28
|
+
i -= 1;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
headers.push([key, value]);
|
|
35
|
+
return;
|
|
36
|
+
} else if (headers instanceof Headers) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
headers.delete(key);
|
|
39
|
+
} else {
|
|
40
|
+
headers.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (Symbol.iterator in headers) {
|
|
45
|
+
headers = options.headers = Object.fromEntries(headers);
|
|
46
|
+
}
|
|
47
|
+
if (value === null) {
|
|
48
|
+
delete headers[key];
|
|
49
|
+
} else {
|
|
50
|
+
headers[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.setHeader = setHeader;
|
|
54
|
+
exports.urlencode = urlencode;
|
package/addons/_utils.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function urlencode(query) {
|
|
2
|
+
const search = new URLSearchParams();
|
|
3
|
+
for (const [key, value] of Object.entries(query)) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
for (let i = 0; i < value.length; i++) {
|
|
6
|
+
search.append(key, String(value[i]));
|
|
7
|
+
}
|
|
8
|
+
} else {
|
|
9
|
+
search.set(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return search;
|
|
13
|
+
}
|
|
14
|
+
function setHeader(options, key, value) {
|
|
15
|
+
if (!options.headers) {
|
|
16
|
+
if (value === null) return;
|
|
17
|
+
options.headers = { [key]: value };
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let { headers } = options;
|
|
21
|
+
if (Array.isArray(headers)) {
|
|
22
|
+
if (value === null) {
|
|
23
|
+
for (let i = 0; i < headers.length; i++) {
|
|
24
|
+
if (headers[i][0] === key) {
|
|
25
|
+
headers.splice(i, 1);
|
|
26
|
+
i -= 1;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
headers.push([key, value]);
|
|
33
|
+
return;
|
|
34
|
+
} else if (headers instanceof Headers) {
|
|
35
|
+
if (value === null) {
|
|
36
|
+
headers.delete(key);
|
|
37
|
+
} else {
|
|
38
|
+
headers.set(key, value);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (Symbol.iterator in headers) {
|
|
43
|
+
headers = options.headers = Object.fromEntries(headers);
|
|
44
|
+
}
|
|
45
|
+
if (value === null) {
|
|
46
|
+
delete headers[key];
|
|
47
|
+
} else {
|
|
48
|
+
headers[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
setHeader,
|
|
53
|
+
urlencode
|
|
54
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const form = require("./form.cjs");
|
|
4
|
+
const multipart = require("./multipart.cjs");
|
|
5
|
+
const addon = require("./parse/addon.cjs");
|
|
6
|
+
const query = require("./query.cjs");
|
|
7
|
+
const rateLimit = require("./rate-limit.cjs");
|
|
8
|
+
const retry = require("./retry.cjs");
|
|
9
|
+
const timeout = require("./timeout.cjs");
|
|
10
|
+
exports.form = form.form;
|
|
11
|
+
exports.multipart = multipart.multipart;
|
|
12
|
+
exports.parser = addon.parser;
|
|
13
|
+
exports.query = query.query;
|
|
14
|
+
exports.rateLimitHandler = rateLimit.rateLimitHandler;
|
|
15
|
+
exports.RetriesExceededError = retry.RetriesExceededError;
|
|
16
|
+
exports.retry = retry.retry;
|
|
17
|
+
exports.TimeoutError = timeout.TimeoutError;
|
|
18
|
+
exports.timeout = timeout.timeout;
|
package/addons/bundle.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { form } from "./form.js";
|
|
2
|
+
import { multipart } from "./multipart.js";
|
|
3
|
+
import { parser } from "./parse/addon.js";
|
|
4
|
+
import { query } from "./query.js";
|
|
5
|
+
import { rateLimitHandler } from "./rate-limit.js";
|
|
6
|
+
import { RetriesExceededError, retry } from "./retry.js";
|
|
7
|
+
import { TimeoutError, timeout } from "./timeout.js";
|
|
8
|
+
export {
|
|
9
|
+
RetriesExceededError,
|
|
10
|
+
TimeoutError,
|
|
11
|
+
form,
|
|
12
|
+
multipart,
|
|
13
|
+
parser,
|
|
14
|
+
query,
|
|
15
|
+
rateLimitHandler,
|
|
16
|
+
retry,
|
|
17
|
+
timeout
|
|
18
|
+
};
|
package/addons/form.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const _utils = require("./_utils.cjs");
|
|
4
|
+
function defaultSerialize(data) {
|
|
5
|
+
return _utils.urlencode(data).toString();
|
|
6
|
+
}
|
|
7
|
+
function form(options = {}) {
|
|
8
|
+
const { serialize = defaultSerialize } = options;
|
|
9
|
+
return {
|
|
10
|
+
beforeRequest: (ctx) => {
|
|
11
|
+
if (ctx.options.form != null || ctx.baseOptions.form != null) {
|
|
12
|
+
if (ctx.options.body != null) {
|
|
13
|
+
throw new Error("Cannot set both form and body");
|
|
14
|
+
}
|
|
15
|
+
const obj = ctx.options.form ?? ctx.baseOptions.form;
|
|
16
|
+
ctx.options.body = serialize(obj);
|
|
17
|
+
ctx.options.method ??= "POST";
|
|
18
|
+
_utils.setHeader(ctx.options, "Content-Type", "application/x-www-form-urlencoded");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
exports.form = form;
|
package/addons/form.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FfetchAddon } from './types.js';
|
|
2
|
+
export interface FormAddon {
|
|
3
|
+
/**
|
|
4
|
+
* shorthand for sending form body,
|
|
5
|
+
* mutually exclusive with other body options
|
|
6
|
+
*
|
|
7
|
+
* if form is passed in base options, passing one
|
|
8
|
+
* in the request options will override it completely
|
|
9
|
+
*/
|
|
10
|
+
form?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface FormAddonOptions {
|
|
13
|
+
/**
|
|
14
|
+
* serializer for the form data.
|
|
15
|
+
* given the form data it should return the serialized data
|
|
16
|
+
*
|
|
17
|
+
* @defaults `URLSearchParams`-based serializer
|
|
18
|
+
* @example `serialize({ a: 123, b: 'hello' }) => 'a=123&b=hello'`
|
|
19
|
+
*/
|
|
20
|
+
serialize?: (data: Record<string, unknown>) => BodyInit;
|
|
21
|
+
}
|
|
22
|
+
export declare function form(options?: FormAddonOptions): FfetchAddon<FormAddon, object>;
|
package/addons/form.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { setHeader, urlencode } from "./_utils.js";
|
|
2
|
+
function defaultSerialize(data) {
|
|
3
|
+
return urlencode(data).toString();
|
|
4
|
+
}
|
|
5
|
+
function form(options = {}) {
|
|
6
|
+
const { serialize = defaultSerialize } = options;
|
|
7
|
+
return {
|
|
8
|
+
beforeRequest: (ctx) => {
|
|
9
|
+
if (ctx.options.form != null || ctx.baseOptions.form != null) {
|
|
10
|
+
if (ctx.options.body != null) {
|
|
11
|
+
throw new Error("Cannot set both form and body");
|
|
12
|
+
}
|
|
13
|
+
const obj = ctx.options.form ?? ctx.baseOptions.form;
|
|
14
|
+
ctx.options.body = serialize(obj);
|
|
15
|
+
ctx.options.method ??= "POST";
|
|
16
|
+
setHeader(ctx.options, "Content-Type", "application/x-www-form-urlencoded");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
form
|
|
23
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const _utils = require("./_utils.cjs");
|
|
4
|
+
function defaultSerialize(data) {
|
|
5
|
+
const formData = new FormData();
|
|
6
|
+
for (const [key, value] of Object.entries(data)) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
for (let i = 0; i < value.length; i++) {
|
|
9
|
+
formData.append(key, String(value[i]));
|
|
10
|
+
}
|
|
11
|
+
} else if (value instanceof File) {
|
|
12
|
+
formData.append(key, value, value.name);
|
|
13
|
+
} else {
|
|
14
|
+
formData.append(key, String(value));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return formData;
|
|
18
|
+
}
|
|
19
|
+
function multipart(options = {}) {
|
|
20
|
+
const { serialize = defaultSerialize } = options;
|
|
21
|
+
return {
|
|
22
|
+
beforeRequest: (ctx) => {
|
|
23
|
+
if (ctx.options.multipart != null || ctx.baseOptions.multipart != null) {
|
|
24
|
+
if (ctx.options.body != null) {
|
|
25
|
+
throw new Error("Cannot set both multipart and body");
|
|
26
|
+
}
|
|
27
|
+
const obj = ctx.options.multipart ?? ctx.baseOptions.multipart;
|
|
28
|
+
ctx.options.body = serialize(obj);
|
|
29
|
+
ctx.options.method ??= "POST";
|
|
30
|
+
_utils.setHeader(ctx.options, "Content-Type", null);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
exports.multipart = multipart;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FfetchAddon } from './types.js';
|
|
2
|
+
export interface MultipartAddon {
|
|
3
|
+
/**
|
|
4
|
+
* shorthand for sending multipart form body,
|
|
5
|
+
* useful for file uploads and similar.
|
|
6
|
+
* mutually exclusive with other body options
|
|
7
|
+
*
|
|
8
|
+
* if multipart is passed in base options, passing one
|
|
9
|
+
* in the request options will override it completely
|
|
10
|
+
*/
|
|
11
|
+
multipart?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface MultipartAddonOptions {
|
|
14
|
+
/**
|
|
15
|
+
* serializer for the form data.
|
|
16
|
+
* given the form data it should return the body
|
|
17
|
+
*
|
|
18
|
+
* @defaults basic `FormData`-based serializer
|
|
19
|
+
*/
|
|
20
|
+
serialize?: (data: Record<string, unknown>) => FormData;
|
|
21
|
+
}
|
|
22
|
+
export declare function multipart(options?: MultipartAddonOptions): FfetchAddon<MultipartAddon, object>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { setHeader } from "./_utils.js";
|
|
2
|
+
function defaultSerialize(data) {
|
|
3
|
+
const formData = new FormData();
|
|
4
|
+
for (const [key, value] of Object.entries(data)) {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
for (let i = 0; i < value.length; i++) {
|
|
7
|
+
formData.append(key, String(value[i]));
|
|
8
|
+
}
|
|
9
|
+
} else if (value instanceof File) {
|
|
10
|
+
formData.append(key, value, value.name);
|
|
11
|
+
} else {
|
|
12
|
+
formData.append(key, String(value));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return formData;
|
|
16
|
+
}
|
|
17
|
+
function multipart(options = {}) {
|
|
18
|
+
const { serialize = defaultSerialize } = options;
|
|
19
|
+
return {
|
|
20
|
+
beforeRequest: (ctx) => {
|
|
21
|
+
if (ctx.options.multipart != null || ctx.baseOptions.multipart != null) {
|
|
22
|
+
if (ctx.options.body != null) {
|
|
23
|
+
throw new Error("Cannot set both multipart and body");
|
|
24
|
+
}
|
|
25
|
+
const obj = ctx.options.multipart ?? ctx.baseOptions.multipart;
|
|
26
|
+
ctx.options.body = serialize(obj);
|
|
27
|
+
ctx.options.method ??= "POST";
|
|
28
|
+
setHeader(ctx.options, "Content-Type", null);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
multipart
|
|
35
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface FfetchTypeProvider {
|
|
2
|
+
readonly schema: unknown;
|
|
3
|
+
readonly parsed: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface FfetchParser<TypeProvider extends FfetchTypeProvider> {
|
|
6
|
+
readonly _provider: TypeProvider;
|
|
7
|
+
parse: (schema: unknown, value: unknown) => unknown | Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export type CallTypeProvider<TypeProvider extends FfetchTypeProvider, Schema> = (TypeProvider & {
|
|
10
|
+
schema: Schema;
|
|
11
|
+
})['parsed'];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BaseIssue, BaseSchema, BaseSchemaAsync, Config, InferOutput } from 'valibot';
|
|
2
|
+
import { FfetchParser, FfetchTypeProvider } from '../_types.js';
|
|
3
|
+
export interface ValibotTypeProvider extends FfetchTypeProvider {
|
|
4
|
+
readonly parsed: this['schema'] extends (BaseSchema<unknown, unknown, BaseIssue<unknown>> | BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>) ? InferOutput<this['schema']> : never;
|
|
5
|
+
}
|
|
6
|
+
export declare function ffetchValibotAdapter({ async, ...rest }?: Partial<Config<BaseIssue<unknown>>> & {
|
|
7
|
+
async?: boolean;
|
|
8
|
+
}): FfetchParser<ValibotTypeProvider>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CastOptions, InferType, ISchema, ValidateOptions } from 'yup';
|
|
2
|
+
import { FfetchParser, FfetchTypeProvider } from '../_types.js';
|
|
3
|
+
export interface YupTypeProvider extends FfetchTypeProvider {
|
|
4
|
+
readonly parsed: this['schema'] extends ISchema<any, any> ? InferType<this['schema']> : never;
|
|
5
|
+
}
|
|
6
|
+
export type FfetchYupAdapterOptions = {
|
|
7
|
+
action: 'cast';
|
|
8
|
+
options?: CastOptions;
|
|
9
|
+
} | {
|
|
10
|
+
action: 'validate';
|
|
11
|
+
options?: ValidateOptions;
|
|
12
|
+
};
|
|
13
|
+
export declare function ffetchYupAdapter({ action, options, }?: FfetchYupAdapterOptions): FfetchParser<YupTypeProvider>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ParseParams, z } from 'zod';
|
|
2
|
+
import { FfetchParser, FfetchTypeProvider } from '../_types.js';
|
|
3
|
+
export interface ZodTypeProvider extends FfetchTypeProvider {
|
|
4
|
+
readonly parsed: this['schema'] extends z.ZodTypeAny ? z.infer<this['schema']> : never;
|
|
5
|
+
}
|
|
6
|
+
export declare function ffetchZodAdapter({ async, ...rest }?: Partial<ParseParams>): FfetchParser<ZodTypeProvider>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function parser(parser2) {
|
|
4
|
+
return {
|
|
5
|
+
response: {
|
|
6
|
+
async parsedJson(schema) {
|
|
7
|
+
return parser2.parse(schema, await this.json());
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
exports.parser = parser;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FfetchAddon } from '../types.js';
|
|
2
|
+
import { CallTypeProvider, FfetchParser, FfetchTypeProvider } from './_types.js';
|
|
3
|
+
export { FfetchParser, FfetchTypeProvider };
|
|
4
|
+
export declare function parser<TypeProvider extends FfetchTypeProvider>(parser: FfetchParser<TypeProvider>): FfetchAddon<object, {
|
|
5
|
+
parsedJson: <Schema>(schema: Schema) => Promise<CallTypeProvider<TypeProvider, Schema>>;
|
|
6
|
+
}>;
|
package/addons/query.cjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const _utils = require("./_utils.cjs");
|
|
4
|
+
function defaultSerialize(query2, url) {
|
|
5
|
+
const params = _utils.urlencode(query2);
|
|
6
|
+
return url + (url.includes("?") ? "&" : "?") + params.toString();
|
|
7
|
+
}
|
|
8
|
+
function query(options = {}) {
|
|
9
|
+
const { serialize = defaultSerialize } = options;
|
|
10
|
+
return {
|
|
11
|
+
beforeRequest: (ctx) => {
|
|
12
|
+
if (ctx.options.query || ctx.baseOptions.query) {
|
|
13
|
+
const obj = ctx.baseOptions.query && ctx.options.query ? {
|
|
14
|
+
...ctx.baseOptions.query,
|
|
15
|
+
...ctx.options.query
|
|
16
|
+
} : ctx.options.query;
|
|
17
|
+
ctx.url = serialize(obj ?? {}, ctx.url);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
exports.query = query;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FfetchAddon } from './types.js';
|
|
2
|
+
export interface QueryAddon {
|
|
3
|
+
/** query params to be appended to the url */
|
|
4
|
+
query?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface QueryAddonOptions {
|
|
7
|
+
/**
|
|
8
|
+
* serializer for the query params.
|
|
9
|
+
* given the query params and the url, it should return the serialized url
|
|
10
|
+
* with the query params added
|
|
11
|
+
*
|
|
12
|
+
* @defaults `URLSearchParams`-based serializer, preserving all existing query params
|
|
13
|
+
* @example `serialize({ a: 123, b: 'hello' }, 'https://example.com/api') => 'https://example.com/api?a=123&b=hello'`
|
|
14
|
+
*/
|
|
15
|
+
serialize?: (query: Record<string, unknown>, url: string) => string;
|
|
16
|
+
}
|
|
17
|
+
export declare function query(options?: QueryAddonOptions): FfetchAddon<QueryAddon, object>;
|
package/addons/query.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { urlencode } from "./_utils.js";
|
|
2
|
+
function defaultSerialize(query2, url) {
|
|
3
|
+
const params = urlencode(query2);
|
|
4
|
+
return url + (url.includes("?") ? "&" : "?") + params.toString();
|
|
5
|
+
}
|
|
6
|
+
function query(options = {}) {
|
|
7
|
+
const { serialize = defaultSerialize } = options;
|
|
8
|
+
return {
|
|
9
|
+
beforeRequest: (ctx) => {
|
|
10
|
+
if (ctx.options.query || ctx.baseOptions.query) {
|
|
11
|
+
const obj = ctx.baseOptions.query && ctx.options.query ? {
|
|
12
|
+
...ctx.baseOptions.query,
|
|
13
|
+
...ctx.options.query
|
|
14
|
+
} : ctx.options.query;
|
|
15
|
+
ctx.url = serialize(obj ?? {}, ctx.url);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
query
|
|
22
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const utils = require("@fuman/utils");
|
|
4
|
+
const defaultIsRejected = (res) => res.status === 429;
|
|
5
|
+
const defaultGetReset = (res) => res.headers.get("x-ratelimit-reset");
|
|
6
|
+
function tryParseDate(str) {
|
|
7
|
+
if (str == null) return null;
|
|
8
|
+
if (typeof str === "number") return str * 1e3;
|
|
9
|
+
const asNum = Number(str);
|
|
10
|
+
if (!Number.isNaN(asNum)) return asNum * 1e3;
|
|
11
|
+
const asDate = new Date(str);
|
|
12
|
+
if (asDate.toString() === "Invalid Date") return null;
|
|
13
|
+
return asDate.getTime();
|
|
14
|
+
}
|
|
15
|
+
function rateLimitMiddleware(options) {
|
|
16
|
+
const {
|
|
17
|
+
isRejected = defaultIsRejected,
|
|
18
|
+
getReset = defaultGetReset,
|
|
19
|
+
defaultWaitTime = 3e4,
|
|
20
|
+
maxWaitTime = 3e5,
|
|
21
|
+
jitter = 5e3,
|
|
22
|
+
maxRetries = 5,
|
|
23
|
+
onRateLimitExceeded
|
|
24
|
+
} = options;
|
|
25
|
+
return async (req, next) => {
|
|
26
|
+
let attempts = 0;
|
|
27
|
+
while (true) {
|
|
28
|
+
if (attempts > maxRetries) throw new Error("Rate limit exceeded, maximum retries exceeded");
|
|
29
|
+
attempts += 1;
|
|
30
|
+
const res = await next(req);
|
|
31
|
+
const rejected = await isRejected(res);
|
|
32
|
+
if (!rejected) return res;
|
|
33
|
+
const reset = tryParseDate(await getReset(res));
|
|
34
|
+
let waitTime;
|
|
35
|
+
if (reset == null) {
|
|
36
|
+
waitTime = defaultWaitTime;
|
|
37
|
+
} else {
|
|
38
|
+
waitTime = reset - Date.now() + jitter;
|
|
39
|
+
if (waitTime < 0) {
|
|
40
|
+
waitTime = void 0;
|
|
41
|
+
} else if (waitTime > maxWaitTime) {
|
|
42
|
+
throw new Error(`Rate limit exceeded, reset time is too far in the future: ${new Date(reset).toISOString()}`, { cause: res });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (waitTime == null) {
|
|
46
|
+
onRateLimitExceeded?.(res, 0);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
onRateLimitExceeded?.(res, waitTime);
|
|
50
|
+
await utils.sleep(waitTime);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function rateLimitHandler() {
|
|
55
|
+
return {
|
|
56
|
+
beforeRequest: (ctx) => {
|
|
57
|
+
if (ctx.options.rateLimit != null || ctx.baseOptions.rateLimit != null) {
|
|
58
|
+
const options = { ...ctx.baseOptions.rateLimit, ...ctx.options.rateLimit };
|
|
59
|
+
ctx.options.middlewares ??= [];
|
|
60
|
+
ctx.options.middlewares?.push(rateLimitMiddleware(options));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
exports.rateLimitHandler = rateLimitHandler;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FfetchAddon } from './types.js';
|
|
2
|
+
import { MaybePromise } from '@fuman/utils';
|
|
3
|
+
export interface RateLimitAddon {
|
|
4
|
+
rateLimit?: {
|
|
5
|
+
/**
|
|
6
|
+
* check if the request was rejected due to rate limit
|
|
7
|
+
*
|
|
8
|
+
* @default `res => res.status === 429`
|
|
9
|
+
*/
|
|
10
|
+
isRejected?: (res: Response) => MaybePromise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* getter for the unix timestamp of the next reset
|
|
13
|
+
* can either be a unix timestamp in seconds or an ISO 8601 date string
|
|
14
|
+
*
|
|
15
|
+
* @default `res => res.headers.get('x-ratelimit-reset')`
|
|
16
|
+
*/
|
|
17
|
+
getReset?: (res: Response) => MaybePromise<string | number | null>;
|
|
18
|
+
/**
|
|
19
|
+
* when the rate limit is exceeded (i.e. `isRejected` returns true),
|
|
20
|
+
* but the reset time is unknown (i.e. `getReset` returns `null`),
|
|
21
|
+
* what is the default time to wait until the rate limit is reset?
|
|
22
|
+
* in milliseconds
|
|
23
|
+
*
|
|
24
|
+
* @default `30_000`
|
|
25
|
+
*/
|
|
26
|
+
defaultWaitTime?: number;
|
|
27
|
+
/**
|
|
28
|
+
* number of milliseconds to add to the reset time when the rate limit is exceeded,
|
|
29
|
+
* to account for network latency and other factors
|
|
30
|
+
*
|
|
31
|
+
* @default `5000`
|
|
32
|
+
*/
|
|
33
|
+
jitter?: number;
|
|
34
|
+
/**
|
|
35
|
+
* when the rate limit has exceeded (i.e. `isRejected` returns true),
|
|
36
|
+
* what is the maximum acceptable time to wait until the rate limit is reset?
|
|
37
|
+
* in milliseconds
|
|
38
|
+
*
|
|
39
|
+
* @default `300_000`
|
|
40
|
+
*/
|
|
41
|
+
maxWaitTime?: number;
|
|
42
|
+
/**
|
|
43
|
+
* maximum number of retries
|
|
44
|
+
*
|
|
45
|
+
* @default `3`
|
|
46
|
+
*/
|
|
47
|
+
maxRetries?: number;
|
|
48
|
+
/**
|
|
49
|
+
* function that will be called when the rate limit is exceeded (i.e. `isRejected` returns true),
|
|
50
|
+
* but before starting the wait timer
|
|
51
|
+
*
|
|
52
|
+
* @param res the response that caused the rate limit to be exceeded
|
|
53
|
+
* @param waitTime the time to wait until the rate limit is reset (in milliseconds)
|
|
54
|
+
*/
|
|
55
|
+
onRateLimitExceeded?: (res: Response, waitTime: number) => void;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* ffetch addon that handles "rate limit exceeded" errors,
|
|
60
|
+
* and waits until the rate limit is reset
|
|
61
|
+
*/
|
|
62
|
+
export declare function rateLimitHandler(): FfetchAddon<RateLimitAddon, object>;
|