@fuman/fetch 0.0.4 → 0.0.6
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/_types.d.cts +8 -0
- package/addons/_utils.d.cts +3 -0
- package/addons/bundle.d.cts +7 -0
- package/addons/form.d.cts +22 -0
- package/addons/index.d.cts +3 -0
- package/addons/multipart.d.cts +22 -0
- package/addons/parse/_types.d.cts +11 -0
- package/addons/parse/adapters/valibot.d.cts +8 -0
- package/addons/parse/adapters/valita.d.cts +8 -0
- package/addons/parse/adapters/yup.d.cts +13 -0
- package/addons/parse/adapters/zod.d.cts +6 -0
- package/addons/parse/addon.d.cts +6 -0
- package/addons/query.d.cts +17 -0
- package/addons/rate-limit.d.cts +62 -0
- package/addons/retry.d.cts +59 -0
- package/addons/retry.d.ts +3 -2
- package/addons/timeout.d.cts +25 -0
- package/addons/tough-cookie.d.cts +7 -0
- package/addons/types.d.cts +30 -0
- package/default.cjs +3 -1
- package/default.d.cts +31 -0
- package/default.d.ts +4 -3
- package/default.js +3 -1
- package/ffetch.cjs +26 -4
- package/ffetch.d.cts +115 -0
- package/ffetch.d.ts +15 -1
- package/ffetch.js +27 -5
- package/index.d.cts +4 -0
- package/package.json +1 -1
- package/tough.d.cts +1 -0
- package/valibot.d.cts +1 -0
- package/valita.d.cts +1 -0
- package/yup.d.cts +1 -0
- package/zod.d.cts +1 -0
package/_types.d.cts
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,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>;
|
|
@@ -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,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,8 @@
|
|
|
1
|
+
import { FfetchParser, FfetchTypeProvider } from '../_types.js';
|
|
2
|
+
import type * as v from '@badrap/valita';
|
|
3
|
+
export interface ValitaTypeProvider extends FfetchTypeProvider {
|
|
4
|
+
readonly parsed: this['schema'] extends v.Type<any> ? v.Infer<this['schema']> : never;
|
|
5
|
+
}
|
|
6
|
+
type ParseOptions = NonNullable<Parameters<v.Type<any>['parse']>[1]>;
|
|
7
|
+
export declare function ffetchValitaAdapter(options?: ParseOptions): FfetchParser<ValitaTypeProvider>;
|
|
8
|
+
export {};
|
|
@@ -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,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
|
+
}>;
|
|
@@ -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>;
|
|
@@ -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>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FfetchAddon } from './types.js';
|
|
2
|
+
export declare class RetriesExceededError extends Error {
|
|
3
|
+
readonly retries: number;
|
|
4
|
+
readonly request: Request;
|
|
5
|
+
constructor(retries: number, request: Request);
|
|
6
|
+
}
|
|
7
|
+
export interface RetryOptions {
|
|
8
|
+
/**
|
|
9
|
+
* max number of retries
|
|
10
|
+
* @default 5
|
|
11
|
+
*/
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
/**
|
|
14
|
+
* delay between retries
|
|
15
|
+
* @default retryCount * 1000, up to 5000
|
|
16
|
+
*/
|
|
17
|
+
retryDelay?: number | ((retryCount: number) => number);
|
|
18
|
+
/**
|
|
19
|
+
* function that will be called before starting the retry loop.
|
|
20
|
+
* if it returns false, the retry loop will be skipped and
|
|
21
|
+
* the error will be thrown immediately
|
|
22
|
+
*
|
|
23
|
+
* @default () => false
|
|
24
|
+
*/
|
|
25
|
+
skip?: (request: Request) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* function that will be called before a retry is attempted,
|
|
28
|
+
* and can be used to modify the request before proceeding
|
|
29
|
+
*
|
|
30
|
+
* @param attempt current retry attempt (starts at 0)
|
|
31
|
+
*/
|
|
32
|
+
onRetry?: (attempt: number, request: Request) => Request | void;
|
|
33
|
+
/**
|
|
34
|
+
* function that will be called whenever a response is received,
|
|
35
|
+
* and should return whether the response is valid (i.e. should be returned and not retried)
|
|
36
|
+
*
|
|
37
|
+
* @default `response => response.status < 500`
|
|
38
|
+
*/
|
|
39
|
+
onResponse?: (response: Response, request: Request) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* function that will be called if an error is thrown while calling
|
|
42
|
+
* the rest of the middleware chain,
|
|
43
|
+
* and should return whether the error should be retried
|
|
44
|
+
*
|
|
45
|
+
* @default `() => true`
|
|
46
|
+
*/
|
|
47
|
+
onError?: (err: unknown, request: Request) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* if true, the last response will be returned if the number of retries is exceeded
|
|
50
|
+
* instead of throwing {@link RetriesExceededError}
|
|
51
|
+
*
|
|
52
|
+
* @default false
|
|
53
|
+
*/
|
|
54
|
+
returnLastResponse?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface RetryAddon {
|
|
57
|
+
retry?: RetryOptions | false;
|
|
58
|
+
}
|
|
59
|
+
export declare function retry(): FfetchAddon<RetryAddon, object>;
|
package/addons/retry.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface RetryOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
returnLastResponse?: boolean;
|
|
55
55
|
}
|
|
56
|
-
export
|
|
56
|
+
export interface RetryAddon {
|
|
57
57
|
retry?: RetryOptions | false;
|
|
58
|
-
}
|
|
58
|
+
}
|
|
59
|
+
export declare function retry(): FfetchAddon<RetryAddon, object>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FetchAddonCtx, FfetchAddon } from './types.js';
|
|
2
|
+
export declare class TimeoutError extends Error {
|
|
3
|
+
readonly timeout: number;
|
|
4
|
+
constructor(timeout: number);
|
|
5
|
+
}
|
|
6
|
+
export interface TimeoutAddon {
|
|
7
|
+
/**
|
|
8
|
+
* timeout for the request in ms
|
|
9
|
+
*
|
|
10
|
+
* pass `Infinity` or `0` to disable the default timeout from the base options
|
|
11
|
+
*
|
|
12
|
+
* when the timeout is reached, the request will be aborted
|
|
13
|
+
* and the promise will be rejected with a TimeoutError
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number | ((ctx: FetchAddonCtx<TimeoutAddon>) => number);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* ffetch addon that allows setting a timeout for the request.
|
|
19
|
+
* when the timeout is reached, the request will be aborted
|
|
20
|
+
* and the promise will be rejected with a TimeoutError
|
|
21
|
+
*
|
|
22
|
+
* **note**: it is important to put this addon as the last one,
|
|
23
|
+
* otherwise other middlewares might be counted towards the timeout
|
|
24
|
+
*/
|
|
25
|
+
export declare function timeout(): FfetchAddon<TimeoutAddon, object>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CookieJar } from 'tough-cookie';
|
|
2
|
+
import { FfetchAddon } from './types.js';
|
|
3
|
+
export interface FfetchToughCookieAddon {
|
|
4
|
+
/** cookie jar to use */
|
|
5
|
+
cookies?: CookieJar;
|
|
6
|
+
}
|
|
7
|
+
export declare function toughCookieAddon(): FfetchAddon<FfetchToughCookieAddon, object>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FfetchOptions, FfetchResult } from '../ffetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* context that is passed to each addon in the order they were added
|
|
4
|
+
* you can safely modify anything in this object
|
|
5
|
+
*/
|
|
6
|
+
export interface FetchAddonCtx<RequestMixin extends object> {
|
|
7
|
+
/** url of the request (with baseUrl already applied) */
|
|
8
|
+
url: string;
|
|
9
|
+
/** options of this specific request */
|
|
10
|
+
options: FfetchOptions & RequestMixin;
|
|
11
|
+
/** base options passed to `createFfetch` */
|
|
12
|
+
baseOptions: FfetchOptions & RequestMixin;
|
|
13
|
+
}
|
|
14
|
+
/** internals that are exposed to the functions in response mixin */
|
|
15
|
+
export type FfetchResultInternals<RequestMixin extends object> = FfetchResult & {
|
|
16
|
+
/** final url of the request */
|
|
17
|
+
_url: string;
|
|
18
|
+
/** request init object that will be passed to fetch */
|
|
19
|
+
_init: RequestInit;
|
|
20
|
+
/** finalized and merged options */
|
|
21
|
+
_options: FfetchOptions & RequestMixin;
|
|
22
|
+
/** finalized and merged headers */
|
|
23
|
+
_headers?: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
export interface FfetchAddon<RequestMixin extends object, ResponseMixin extends object> {
|
|
26
|
+
/** function that will be called before each request */
|
|
27
|
+
beforeRequest?: (ctx: FetchAddonCtx<RequestMixin>) => void;
|
|
28
|
+
/** mixin functions that will be added to the response promise */
|
|
29
|
+
response?: ResponseMixin;
|
|
30
|
+
}
|
package/default.cjs
CHANGED
|
@@ -5,11 +5,13 @@ const timeout = require("./addons/timeout.cjs");
|
|
|
5
5
|
const query = require("./addons/query.cjs");
|
|
6
6
|
const form = require("./addons/form.cjs");
|
|
7
7
|
const multipart = require("./addons/multipart.cjs");
|
|
8
|
+
const retry = require("./addons/retry.cjs");
|
|
8
9
|
const ffetchDefaultAddons = [
|
|
9
10
|
/* @__PURE__ */ timeout.timeout(),
|
|
10
11
|
/* @__PURE__ */ query.query(),
|
|
11
12
|
/* @__PURE__ */ form.form(),
|
|
12
|
-
/* @__PURE__ */ multipart.multipart()
|
|
13
|
+
/* @__PURE__ */ multipart.multipart(),
|
|
14
|
+
/* @__PURE__ */ retry.retry()
|
|
13
15
|
];
|
|
14
16
|
const ffetchBase = /* @__PURE__ */ ffetch.createFfetch({
|
|
15
17
|
addons: ffetchDefaultAddons
|
package/default.d.cts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FfetchAddon, ffetchAddons } from './addons/index.js';
|
|
2
|
+
import { Ffetch } from './ffetch.js';
|
|
3
|
+
export declare const ffetchDefaultAddons: [
|
|
4
|
+
FfetchAddon<ffetchAddons.TimeoutAddon, object>,
|
|
5
|
+
FfetchAddon<ffetchAddons.QueryAddon, object>,
|
|
6
|
+
FfetchAddon<ffetchAddons.FormAddon, object>,
|
|
7
|
+
FfetchAddon<ffetchAddons.MultipartAddon, object>,
|
|
8
|
+
FfetchAddon<ffetchAddons.RetryAddon, object>
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* the default ffetch instance with a reasonable default set of addons
|
|
12
|
+
*
|
|
13
|
+
* you can use this as a base to create your project-specific fetch instance,
|
|
14
|
+
* or use this as is.
|
|
15
|
+
*
|
|
16
|
+
* this is not exported as `ffetch` because most of the time you will want to extend it,
|
|
17
|
+
* and exporting it as `ffetch` would make them clash in import suggestions,
|
|
18
|
+
* and will also make it prone to subtle bugs.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { ffetchBase } from '@fuman/fetch'
|
|
23
|
+
*
|
|
24
|
+
* const ffetch = ffetchBase.extend({
|
|
25
|
+
* baseUrl: 'https://example.com',
|
|
26
|
+
* headers: { ... },
|
|
27
|
+
* addons: [ ... ],
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon & ffetchAddons.RetryAddon, object>;
|
package/default.d.ts
CHANGED
|
@@ -4,10 +4,11 @@ export declare const ffetchDefaultAddons: [
|
|
|
4
4
|
FfetchAddon<ffetchAddons.TimeoutAddon, object>,
|
|
5
5
|
FfetchAddon<ffetchAddons.QueryAddon, object>,
|
|
6
6
|
FfetchAddon<ffetchAddons.FormAddon, object>,
|
|
7
|
-
FfetchAddon<ffetchAddons.MultipartAddon, object
|
|
7
|
+
FfetchAddon<ffetchAddons.MultipartAddon, object>,
|
|
8
|
+
FfetchAddon<ffetchAddons.RetryAddon, object>
|
|
8
9
|
];
|
|
9
10
|
/**
|
|
10
|
-
* the default ffetch instance with reasonable default set of addons
|
|
11
|
+
* the default ffetch instance with a reasonable default set of addons
|
|
11
12
|
*
|
|
12
13
|
* you can use this as a base to create your project-specific fetch instance,
|
|
13
14
|
* or use this as is.
|
|
@@ -27,4 +28,4 @@ export declare const ffetchDefaultAddons: [
|
|
|
27
28
|
* })
|
|
28
29
|
* ```
|
|
29
30
|
*/
|
|
30
|
-
export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon, object>;
|
|
31
|
+
export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon & ffetchAddons.RetryAddon, object>;
|
package/default.js
CHANGED
|
@@ -3,11 +3,13 @@ import { timeout } from "./addons/timeout.js";
|
|
|
3
3
|
import { query } from "./addons/query.js";
|
|
4
4
|
import { form } from "./addons/form.js";
|
|
5
5
|
import { multipart } from "./addons/multipart.js";
|
|
6
|
+
import { retry } from "./addons/retry.js";
|
|
6
7
|
const ffetchDefaultAddons = [
|
|
7
8
|
/* @__PURE__ */ timeout(),
|
|
8
9
|
/* @__PURE__ */ query(),
|
|
9
10
|
/* @__PURE__ */ form(),
|
|
10
|
-
/* @__PURE__ */ multipart()
|
|
11
|
+
/* @__PURE__ */ multipart(),
|
|
12
|
+
/* @__PURE__ */ retry()
|
|
11
13
|
];
|
|
12
14
|
const ffetchBase = /* @__PURE__ */ createFfetch({
|
|
13
15
|
addons: ffetchDefaultAddons
|
package/ffetch.cjs
CHANGED
|
@@ -7,6 +7,8 @@ class HttpError extends Error {
|
|
|
7
7
|
super(`HTTP Error ${response.status} ${response.statusText}`);
|
|
8
8
|
this.response = response;
|
|
9
9
|
}
|
|
10
|
+
body = null;
|
|
11
|
+
bodyText = null;
|
|
10
12
|
}
|
|
11
13
|
function headersToObject(headers) {
|
|
12
14
|
if (!headers) return {};
|
|
@@ -45,15 +47,27 @@ class FfetchResultImpl {
|
|
|
45
47
|
}
|
|
46
48
|
async #fetchAndValidate() {
|
|
47
49
|
const res = await this.#fetch(new Request(this._url, this._init));
|
|
50
|
+
let err = null;
|
|
48
51
|
if (this._options.validateResponse === void 0 || this._options.validateResponse !== false) {
|
|
49
52
|
if (typeof this._options.validateResponse === "function") {
|
|
50
53
|
if (!await this._options.validateResponse(res)) {
|
|
51
|
-
|
|
54
|
+
err = new HttpError(res);
|
|
52
55
|
}
|
|
53
56
|
} else if (!res.ok) {
|
|
54
|
-
|
|
57
|
+
err = new HttpError(res);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
if (err != null) {
|
|
61
|
+
if (this._options.readBodyOnError !== false) {
|
|
62
|
+
try {
|
|
63
|
+
;
|
|
64
|
+
err.body = new Uint8Array(await res.arrayBuffer());
|
|
65
|
+
err.bodyText = utils.utf8.decoder.decode(err.body);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
57
71
|
return res;
|
|
58
72
|
}
|
|
59
73
|
async raw() {
|
|
@@ -142,8 +156,16 @@ function createFfetch(baseOptions = {}) {
|
|
|
142
156
|
if (options.middlewares !== void 0 && options.middlewares.length > 0) {
|
|
143
157
|
fetcher = utils.composeMiddlewares(options.middlewares, wrappedFetch);
|
|
144
158
|
}
|
|
145
|
-
if (baseOptions?.baseUrl != null || options.baseUrl != null) {
|
|
146
|
-
|
|
159
|
+
if ((baseOptions?.baseUrl != null || options.baseUrl != null) && !url.includes("://")) {
|
|
160
|
+
const baseUrl = options.baseUrl ?? baseOptions?.baseUrl;
|
|
161
|
+
let prepend = baseUrl;
|
|
162
|
+
if (prepend[prepend.length - 1] !== "/") {
|
|
163
|
+
prepend += "/";
|
|
164
|
+
}
|
|
165
|
+
if (url[0] === "/") {
|
|
166
|
+
url = url.slice(1);
|
|
167
|
+
}
|
|
168
|
+
url = prepend + url;
|
|
147
169
|
}
|
|
148
170
|
let init;
|
|
149
171
|
let headers;
|
package/ffetch.d.cts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { CombineAddons, FfetchMiddleware } from './_types.js';
|
|
2
|
+
import { FfetchAddon } from './addons/types.js';
|
|
3
|
+
export interface FfetchOptions {
|
|
4
|
+
/**
|
|
5
|
+
* http method
|
|
6
|
+
* @default 'GET'
|
|
7
|
+
*/
|
|
8
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | (string & {});
|
|
9
|
+
/**
|
|
10
|
+
* whether to throw HttpError on non-2xx responses
|
|
11
|
+
*
|
|
12
|
+
* when a function is provided, it will be called with the response
|
|
13
|
+
* and should return whether the response is valid.
|
|
14
|
+
* if it returns false, the response will be thrown as an HttpError
|
|
15
|
+
*
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
validateResponse?: false | ((res: Response) => boolean | Promise<boolean>);
|
|
19
|
+
/**
|
|
20
|
+
* whether to read the body of the response on HttpError (i.e. when `validateResponse` returns false).
|
|
21
|
+
* useful for debugging, but may be undesirable in some cases.
|
|
22
|
+
*
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
readBodyOnError?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* base url to be prepended to the url
|
|
28
|
+
*
|
|
29
|
+
* this base url is **always** treated as a "base path", i.e. the path passed to `ffetch()`
|
|
30
|
+
* will always be appended to it (unlike the `new URL()`, which has ambiguous slash semantics)
|
|
31
|
+
*/
|
|
32
|
+
baseUrl?: string;
|
|
33
|
+
/** body to be passed to fetch() */
|
|
34
|
+
body?: BodyInit;
|
|
35
|
+
/**
|
|
36
|
+
* shorthand for sending json body.
|
|
37
|
+
* mutually exclusive with `body`
|
|
38
|
+
*/
|
|
39
|
+
json?: unknown;
|
|
40
|
+
/** headers to be passed to fetch() */
|
|
41
|
+
headers?: HeadersInit;
|
|
42
|
+
/** middlewares for the requests */
|
|
43
|
+
middlewares?: FfetchMiddleware[];
|
|
44
|
+
/** any additional options to be passed to fetch() */
|
|
45
|
+
extra?: RequestInit;
|
|
46
|
+
}
|
|
47
|
+
export interface FfetchBaseOptions<Addons extends FfetchAddon<any, any>[] = FfetchAddon<any, any>[]> extends FfetchOptions {
|
|
48
|
+
/** implementation of fetch() */
|
|
49
|
+
fetch?: typeof fetch;
|
|
50
|
+
/** addons for the request */
|
|
51
|
+
addons?: Addons;
|
|
52
|
+
/**
|
|
53
|
+
* whether to capture stack trace for errors
|
|
54
|
+
* may slightly impact performance
|
|
55
|
+
*
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
captureStackTrace?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface FfetchResult extends Promise<Response> {
|
|
61
|
+
raw: () => Promise<Response>;
|
|
62
|
+
stream: () => Promise<ReadableStream<Uint8Array>>;
|
|
63
|
+
json: <T = unknown>() => Promise<T>;
|
|
64
|
+
text: () => Promise<string>;
|
|
65
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
66
|
+
blob: () => Promise<Blob>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* the main function of the library
|
|
70
|
+
*
|
|
71
|
+
* @param url url to fetch (or path, if baseUrl is set)
|
|
72
|
+
* @param params options (note that the function may mutate the object, do not rely on its immutability)
|
|
73
|
+
*/
|
|
74
|
+
export interface Ffetch<RequestMixin, ResponseMixin> {
|
|
75
|
+
(url: string, params?: FfetchOptions & RequestMixin): FfetchResult & ResponseMixin;
|
|
76
|
+
/** shorthand for making a GET request */
|
|
77
|
+
get: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
78
|
+
/** shorthand for making a POST request */
|
|
79
|
+
post: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
80
|
+
/** shorthand for making a PUT request */
|
|
81
|
+
put: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
82
|
+
/** shorthand for making a DELETE request */
|
|
83
|
+
delete: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
84
|
+
/** shorthand for making a PATCH request */
|
|
85
|
+
patch: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
86
|
+
/** shorthand for making a HEAD request */
|
|
87
|
+
head: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
88
|
+
/** shorthand for making an OPTIONS request */
|
|
89
|
+
options: (url: string, params?: FfetchOptions & RequestMixin) => FfetchResult & ResponseMixin;
|
|
90
|
+
/**
|
|
91
|
+
* extend the base options with the given options
|
|
92
|
+
*
|
|
93
|
+
* note: addons, middlewares and headers will be merged with the base options,
|
|
94
|
+
* the rest of the options will be overridden
|
|
95
|
+
*/
|
|
96
|
+
extend: <const Addons extends FfetchAddon<any, any>[], Combined extends {
|
|
97
|
+
request: object;
|
|
98
|
+
response: object;
|
|
99
|
+
} = CombineAddons<Addons>>(baseOptions: FfetchBaseOptions<Addons> & Combined['request']) => Ffetch<RequestMixin & Combined['request'], ResponseMixin & Combined['response']>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* an error that is thrown when the response status is not 2xx,
|
|
103
|
+
* or `validateResponse` returns false
|
|
104
|
+
*/
|
|
105
|
+
export declare class HttpError extends Error {
|
|
106
|
+
readonly response: Response;
|
|
107
|
+
readonly body: Uint8Array | null;
|
|
108
|
+
readonly bodyText: string | null;
|
|
109
|
+
constructor(response: Response);
|
|
110
|
+
}
|
|
111
|
+
/** create a new ffetch function with the given base options */
|
|
112
|
+
export declare function createFfetch<const Addons extends FfetchAddon<any, any>[], Combined extends {
|
|
113
|
+
request: object;
|
|
114
|
+
response: object;
|
|
115
|
+
} = CombineAddons<Addons>>(baseOptions?: FfetchBaseOptions<Addons> & Combined['request']): Ffetch<Combined['request'], Combined['response']>;
|
package/ffetch.d.ts
CHANGED
|
@@ -16,7 +16,19 @@ export interface FfetchOptions {
|
|
|
16
16
|
* @default true
|
|
17
17
|
*/
|
|
18
18
|
validateResponse?: false | ((res: Response) => boolean | Promise<boolean>);
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* whether to read the body of the response on HttpError (i.e. when `validateResponse` returns false).
|
|
21
|
+
* useful for debugging, but may be undesirable in some cases.
|
|
22
|
+
*
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
readBodyOnError?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* base url to be prepended to the url
|
|
28
|
+
*
|
|
29
|
+
* this base url is **always** treated as a "base path", i.e. the path passed to `ffetch()`
|
|
30
|
+
* will always be appended to it (unlike the `new URL()`, which has ambiguous slash semantics)
|
|
31
|
+
*/
|
|
20
32
|
baseUrl?: string;
|
|
21
33
|
/** body to be passed to fetch() */
|
|
22
34
|
body?: BodyInit;
|
|
@@ -92,6 +104,8 @@ export interface Ffetch<RequestMixin, ResponseMixin> {
|
|
|
92
104
|
*/
|
|
93
105
|
export declare class HttpError extends Error {
|
|
94
106
|
readonly response: Response;
|
|
107
|
+
readonly body: Uint8Array | null;
|
|
108
|
+
readonly bodyText: string | null;
|
|
95
109
|
constructor(response: Response);
|
|
96
110
|
}
|
|
97
111
|
/** create a new ffetch function with the given base options */
|
package/ffetch.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { composeMiddlewares, unknownToError } from "@fuman/utils";
|
|
1
|
+
import { composeMiddlewares, utf8, unknownToError } from "@fuman/utils";
|
|
2
2
|
const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
|
|
3
3
|
class HttpError extends Error {
|
|
4
4
|
constructor(response) {
|
|
5
5
|
super(`HTTP Error ${response.status} ${response.statusText}`);
|
|
6
6
|
this.response = response;
|
|
7
7
|
}
|
|
8
|
+
body = null;
|
|
9
|
+
bodyText = null;
|
|
8
10
|
}
|
|
9
11
|
function headersToObject(headers) {
|
|
10
12
|
if (!headers) return {};
|
|
@@ -43,15 +45,27 @@ class FfetchResultImpl {
|
|
|
43
45
|
}
|
|
44
46
|
async #fetchAndValidate() {
|
|
45
47
|
const res = await this.#fetch(new Request(this._url, this._init));
|
|
48
|
+
let err = null;
|
|
46
49
|
if (this._options.validateResponse === void 0 || this._options.validateResponse !== false) {
|
|
47
50
|
if (typeof this._options.validateResponse === "function") {
|
|
48
51
|
if (!await this._options.validateResponse(res)) {
|
|
49
|
-
|
|
52
|
+
err = new HttpError(res);
|
|
50
53
|
}
|
|
51
54
|
} else if (!res.ok) {
|
|
52
|
-
|
|
55
|
+
err = new HttpError(res);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
if (err != null) {
|
|
59
|
+
if (this._options.readBodyOnError !== false) {
|
|
60
|
+
try {
|
|
61
|
+
;
|
|
62
|
+
err.body = new Uint8Array(await res.arrayBuffer());
|
|
63
|
+
err.bodyText = utf8.decoder.decode(err.body);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
55
69
|
return res;
|
|
56
70
|
}
|
|
57
71
|
async raw() {
|
|
@@ -140,8 +154,16 @@ function createFfetch(baseOptions = {}) {
|
|
|
140
154
|
if (options.middlewares !== void 0 && options.middlewares.length > 0) {
|
|
141
155
|
fetcher = composeMiddlewares(options.middlewares, wrappedFetch);
|
|
142
156
|
}
|
|
143
|
-
if (baseOptions?.baseUrl != null || options.baseUrl != null) {
|
|
144
|
-
|
|
157
|
+
if ((baseOptions?.baseUrl != null || options.baseUrl != null) && !url.includes("://")) {
|
|
158
|
+
const baseUrl = options.baseUrl ?? baseOptions?.baseUrl;
|
|
159
|
+
let prepend = baseUrl;
|
|
160
|
+
if (prepend[prepend.length - 1] !== "/") {
|
|
161
|
+
prepend += "/";
|
|
162
|
+
}
|
|
163
|
+
if (url[0] === "/") {
|
|
164
|
+
url = url.slice(1);
|
|
165
|
+
}
|
|
166
|
+
url = prepend + url;
|
|
145
167
|
}
|
|
146
168
|
let init;
|
|
147
169
|
let headers;
|
package/index.d.cts
ADDED
package/package.json
CHANGED
package/tough.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./addons/tough-cookie.js"
|
package/valibot.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./addons/parse/adapters/valibot.js"
|
package/valita.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./addons/parse/adapters/valita.js"
|
package/yup.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./addons/parse/adapters/yup.js"
|
package/zod.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./addons/parse/adapters/zod.js"
|