@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.
Files changed (59) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +13 -0
  3. package/_types.d.ts +8 -0
  4. package/addons/_utils.cjs +54 -0
  5. package/addons/_utils.d.ts +3 -0
  6. package/addons/_utils.js +54 -0
  7. package/addons/bundle.cjs +18 -0
  8. package/addons/bundle.d.ts +7 -0
  9. package/addons/bundle.js +18 -0
  10. package/addons/form.cjs +23 -0
  11. package/addons/form.d.ts +22 -0
  12. package/addons/form.js +23 -0
  13. package/addons/index.d.ts +3 -0
  14. package/addons/multipart.cjs +35 -0
  15. package/addons/multipart.d.ts +22 -0
  16. package/addons/multipart.js +35 -0
  17. package/addons/parse/_types.d.ts +11 -0
  18. package/addons/parse/adapters/valibot.d.ts +8 -0
  19. package/addons/parse/adapters/yup.d.ts +13 -0
  20. package/addons/parse/adapters/zod.d.ts +6 -0
  21. package/addons/parse/addon.cjs +12 -0
  22. package/addons/parse/addon.d.ts +6 -0
  23. package/addons/parse/addon.js +12 -0
  24. package/addons/query.cjs +22 -0
  25. package/addons/query.d.ts +17 -0
  26. package/addons/query.js +22 -0
  27. package/addons/rate-limit.cjs +65 -0
  28. package/addons/rate-limit.d.ts +62 -0
  29. package/addons/rate-limit.js +65 -0
  30. package/addons/retry.cjs +74 -0
  31. package/addons/retry.d.ts +58 -0
  32. package/addons/retry.js +74 -0
  33. package/addons/timeout.cjs +54 -0
  34. package/addons/timeout.d.ts +25 -0
  35. package/addons/timeout.js +54 -0
  36. package/addons/tough-cookie.d.ts +7 -0
  37. package/addons/types.d.ts +30 -0
  38. package/default.cjs +18 -0
  39. package/default.d.ts +30 -0
  40. package/default.js +18 -0
  41. package/ffetch.cjs +200 -0
  42. package/ffetch.d.ts +101 -0
  43. package/ffetch.js +200 -0
  44. package/index.cjs +10 -0
  45. package/index.d.ts +4 -0
  46. package/index.js +10 -0
  47. package/package.json +89 -0
  48. package/tough.cjs +24 -0
  49. package/tough.d.ts +1 -0
  50. package/tough.js +24 -0
  51. package/valibot.cjs +16 -0
  52. package/valibot.d.ts +1 -0
  53. package/valibot.js +16 -0
  54. package/yup.cjs +18 -0
  55. package/yup.d.ts +1 -0
  56. package/yup.js +18 -0
  57. package/zod.cjs +15 -0
  58. package/zod.d.ts +1 -0
  59. package/zod.js +15 -0
@@ -0,0 +1,65 @@
1
+ import { sleep } from "@fuman/utils";
2
+ const defaultIsRejected = (res) => res.status === 429;
3
+ const defaultGetReset = (res) => res.headers.get("x-ratelimit-reset");
4
+ function tryParseDate(str) {
5
+ if (str == null) return null;
6
+ if (typeof str === "number") return str * 1e3;
7
+ const asNum = Number(str);
8
+ if (!Number.isNaN(asNum)) return asNum * 1e3;
9
+ const asDate = new Date(str);
10
+ if (asDate.toString() === "Invalid Date") return null;
11
+ return asDate.getTime();
12
+ }
13
+ function rateLimitMiddleware(options) {
14
+ const {
15
+ isRejected = defaultIsRejected,
16
+ getReset = defaultGetReset,
17
+ defaultWaitTime = 3e4,
18
+ maxWaitTime = 3e5,
19
+ jitter = 5e3,
20
+ maxRetries = 5,
21
+ onRateLimitExceeded
22
+ } = options;
23
+ return async (req, next) => {
24
+ let attempts = 0;
25
+ while (true) {
26
+ if (attempts > maxRetries) throw new Error("Rate limit exceeded, maximum retries exceeded");
27
+ attempts += 1;
28
+ const res = await next(req);
29
+ const rejected = await isRejected(res);
30
+ if (!rejected) return res;
31
+ const reset = tryParseDate(await getReset(res));
32
+ let waitTime;
33
+ if (reset == null) {
34
+ waitTime = defaultWaitTime;
35
+ } else {
36
+ waitTime = reset - Date.now() + jitter;
37
+ if (waitTime < 0) {
38
+ waitTime = void 0;
39
+ } else if (waitTime > maxWaitTime) {
40
+ throw new Error(`Rate limit exceeded, reset time is too far in the future: ${new Date(reset).toISOString()}`, { cause: res });
41
+ }
42
+ }
43
+ if (waitTime == null) {
44
+ onRateLimitExceeded?.(res, 0);
45
+ continue;
46
+ }
47
+ onRateLimitExceeded?.(res, waitTime);
48
+ await sleep(waitTime);
49
+ }
50
+ };
51
+ }
52
+ function rateLimitHandler() {
53
+ return {
54
+ beforeRequest: (ctx) => {
55
+ if (ctx.options.rateLimit != null || ctx.baseOptions.rateLimit != null) {
56
+ const options = { ...ctx.baseOptions.rateLimit, ...ctx.options.rateLimit };
57
+ ctx.options.middlewares ??= [];
58
+ ctx.options.middlewares?.push(rateLimitMiddleware(options));
59
+ }
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ rateLimitHandler
65
+ };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const utils = require("@fuman/utils");
4
+ class RetriesExceededError extends Error {
5
+ constructor(retries, request) {
6
+ super(`Retries (${retries}) exceeded for ${request.url}`);
7
+ this.retries = retries;
8
+ this.request = request;
9
+ }
10
+ }
11
+ function defaultRetryDelay(retryCount) {
12
+ if (retryCount >= 5) return 5e3;
13
+ return retryCount * 1e3;
14
+ }
15
+ function retryMiddleware(options) {
16
+ const {
17
+ maxRetries = 5,
18
+ retryDelay = defaultRetryDelay,
19
+ onResponse = (response) => response.status < 500,
20
+ returnLastResponse = false,
21
+ onError,
22
+ onRetry,
23
+ skip
24
+ } = options;
25
+ return async (request, next) => {
26
+ if (skip?.(request)) {
27
+ return next(request);
28
+ }
29
+ let retries = 0;
30
+ while (true) {
31
+ onRetry?.(retries, request);
32
+ try {
33
+ const res = await next(request);
34
+ if (onResponse(res, request)) {
35
+ return res;
36
+ }
37
+ } catch (err) {
38
+ if (onError && !onError(err, request)) {
39
+ throw err;
40
+ }
41
+ }
42
+ if (retries++ >= maxRetries) {
43
+ throw new RetriesExceededError(maxRetries, request);
44
+ }
45
+ await utils.sleep(typeof retryDelay === "function" ? retryDelay(retries) : retryDelay);
46
+ }
47
+ };
48
+ }
49
+ function retry() {
50
+ return {
51
+ beforeRequest: (ctx) => {
52
+ if (ctx.options.retry != null || ctx.baseOptions.retry != null) {
53
+ let options;
54
+ if (ctx.baseOptions.retry != null) {
55
+ if (ctx.options.retry === false) {
56
+ return;
57
+ }
58
+ options = ctx.options.retry ? {
59
+ ...ctx.baseOptions.retry,
60
+ ...ctx.options.retry
61
+ } : ctx.baseOptions.retry;
62
+ } else if (ctx.options.retry === false) {
63
+ return;
64
+ } else {
65
+ options = ctx.options.retry;
66
+ }
67
+ ctx.options.middlewares ??= [];
68
+ ctx.options.middlewares.push(retryMiddleware(options));
69
+ }
70
+ }
71
+ };
72
+ }
73
+ exports.RetriesExceededError = RetriesExceededError;
74
+ exports.retry = retry;
@@ -0,0 +1,58 @@
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 declare function retry(): FfetchAddon<{
57
+ retry?: RetryOptions | false;
58
+ }, object>;
@@ -0,0 +1,74 @@
1
+ import { sleep } from "@fuman/utils";
2
+ class RetriesExceededError extends Error {
3
+ constructor(retries, request) {
4
+ super(`Retries (${retries}) exceeded for ${request.url}`);
5
+ this.retries = retries;
6
+ this.request = request;
7
+ }
8
+ }
9
+ function defaultRetryDelay(retryCount) {
10
+ if (retryCount >= 5) return 5e3;
11
+ return retryCount * 1e3;
12
+ }
13
+ function retryMiddleware(options) {
14
+ const {
15
+ maxRetries = 5,
16
+ retryDelay = defaultRetryDelay,
17
+ onResponse = (response) => response.status < 500,
18
+ returnLastResponse = false,
19
+ onError,
20
+ onRetry,
21
+ skip
22
+ } = options;
23
+ return async (request, next) => {
24
+ if (skip?.(request)) {
25
+ return next(request);
26
+ }
27
+ let retries = 0;
28
+ while (true) {
29
+ onRetry?.(retries, request);
30
+ try {
31
+ const res = await next(request);
32
+ if (onResponse(res, request)) {
33
+ return res;
34
+ }
35
+ } catch (err) {
36
+ if (onError && !onError(err, request)) {
37
+ throw err;
38
+ }
39
+ }
40
+ if (retries++ >= maxRetries) {
41
+ throw new RetriesExceededError(maxRetries, request);
42
+ }
43
+ await sleep(typeof retryDelay === "function" ? retryDelay(retries) : retryDelay);
44
+ }
45
+ };
46
+ }
47
+ function retry() {
48
+ return {
49
+ beforeRequest: (ctx) => {
50
+ if (ctx.options.retry != null || ctx.baseOptions.retry != null) {
51
+ let options;
52
+ if (ctx.baseOptions.retry != null) {
53
+ if (ctx.options.retry === false) {
54
+ return;
55
+ }
56
+ options = ctx.options.retry ? {
57
+ ...ctx.baseOptions.retry,
58
+ ...ctx.options.retry
59
+ } : ctx.baseOptions.retry;
60
+ } else if (ctx.options.retry === false) {
61
+ return;
62
+ } else {
63
+ options = ctx.options.retry;
64
+ }
65
+ ctx.options.middlewares ??= [];
66
+ ctx.options.middlewares.push(retryMiddleware(options));
67
+ }
68
+ }
69
+ };
70
+ }
71
+ export {
72
+ RetriesExceededError,
73
+ retry
74
+ };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const utils = require("@fuman/utils");
4
+ class TimeoutError extends Error {
5
+ constructor(timeout2) {
6
+ super(`Timeout exceeded: ${timeout2}ms`);
7
+ this.timeout = timeout2;
8
+ }
9
+ }
10
+ function timeoutMiddleware(timeout2) {
11
+ return async (request, next) => {
12
+ const controller = new AbortController();
13
+ const timer = utils.timers.setTimeout(() => {
14
+ controller.abort(new TimeoutError(timeout2));
15
+ }, timeout2);
16
+ if (request.signal != null) {
17
+ const signal = request.signal;
18
+ if (signal.aborted) {
19
+ throw signal.reason;
20
+ }
21
+ signal.addEventListener("abort", () => {
22
+ controller.abort(signal.reason);
23
+ utils.timers.clearTimeout(timer);
24
+ });
25
+ }
26
+ request = new Request(request, {
27
+ signal: controller.signal
28
+ });
29
+ try {
30
+ return await next(request);
31
+ } finally {
32
+ utils.timers.clearTimeout(timer);
33
+ }
34
+ };
35
+ }
36
+ function timeout() {
37
+ return {
38
+ beforeRequest: (ctx) => {
39
+ if (ctx.options.timeout != null || ctx.baseOptions.timeout != null) {
40
+ let timeout2 = ctx.options.timeout ?? ctx.baseOptions.timeout;
41
+ if (typeof timeout2 === "function") {
42
+ timeout2 = timeout2(ctx);
43
+ }
44
+ if (timeout2 === Infinity || timeout2 <= 0) {
45
+ return;
46
+ }
47
+ ctx.options.middlewares ??= [];
48
+ ctx.options.middlewares?.push(timeoutMiddleware(timeout2));
49
+ }
50
+ }
51
+ };
52
+ }
53
+ exports.TimeoutError = TimeoutError;
54
+ exports.timeout = timeout;
@@ -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,54 @@
1
+ import { timers } from "@fuman/utils";
2
+ class TimeoutError extends Error {
3
+ constructor(timeout2) {
4
+ super(`Timeout exceeded: ${timeout2}ms`);
5
+ this.timeout = timeout2;
6
+ }
7
+ }
8
+ function timeoutMiddleware(timeout2) {
9
+ return async (request, next) => {
10
+ const controller = new AbortController();
11
+ const timer = timers.setTimeout(() => {
12
+ controller.abort(new TimeoutError(timeout2));
13
+ }, timeout2);
14
+ if (request.signal != null) {
15
+ const signal = request.signal;
16
+ if (signal.aborted) {
17
+ throw signal.reason;
18
+ }
19
+ signal.addEventListener("abort", () => {
20
+ controller.abort(signal.reason);
21
+ timers.clearTimeout(timer);
22
+ });
23
+ }
24
+ request = new Request(request, {
25
+ signal: controller.signal
26
+ });
27
+ try {
28
+ return await next(request);
29
+ } finally {
30
+ timers.clearTimeout(timer);
31
+ }
32
+ };
33
+ }
34
+ function timeout() {
35
+ return {
36
+ beforeRequest: (ctx) => {
37
+ if (ctx.options.timeout != null || ctx.baseOptions.timeout != null) {
38
+ let timeout2 = ctx.options.timeout ?? ctx.baseOptions.timeout;
39
+ if (typeof timeout2 === "function") {
40
+ timeout2 = timeout2(ctx);
41
+ }
42
+ if (timeout2 === Infinity || timeout2 <= 0) {
43
+ return;
44
+ }
45
+ ctx.options.middlewares ??= [];
46
+ ctx.options.middlewares?.push(timeoutMiddleware(timeout2));
47
+ }
48
+ }
49
+ };
50
+ }
51
+ export {
52
+ TimeoutError,
53
+ timeout
54
+ };
@@ -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 ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const ffetch = require("./ffetch.cjs");
4
+ const timeout = require("./addons/timeout.cjs");
5
+ const query = require("./addons/query.cjs");
6
+ const form = require("./addons/form.cjs");
7
+ const multipart = require("./addons/multipart.cjs");
8
+ const ffetchDefaultAddons = [
9
+ /* @__PURE__ */ timeout.timeout(),
10
+ /* @__PURE__ */ query.query(),
11
+ /* @__PURE__ */ form.form(),
12
+ /* @__PURE__ */ multipart.multipart()
13
+ ];
14
+ const ffetchBase = /* @__PURE__ */ ffetch.createFfetch({
15
+ addons: ffetchDefaultAddons
16
+ });
17
+ exports.ffetchBase = ffetchBase;
18
+ exports.ffetchDefaultAddons = ffetchDefaultAddons;
package/default.d.ts ADDED
@@ -0,0 +1,30 @@
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
+ ];
9
+ /**
10
+ * the default ffetch instance with reasonable default set of addons
11
+ *
12
+ * you can use this as a base to create your project-specific fetch instance,
13
+ * or use this as is.
14
+ *
15
+ * this is not exported as `ffetch` because most of the time you will want to extend it,
16
+ * and exporting it as `ffetch` would make them clash in import suggestions,
17
+ * and will also make it prone to subtle bugs.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { ffetchBase } from '@fuman/fetch'
22
+ *
23
+ * const ffetch = ffetchBase.extend({
24
+ * baseUrl: 'https://example.com',
25
+ * headers: { ... },
26
+ * addons: [ ... ],
27
+ * })
28
+ * ```
29
+ */
30
+ export declare const ffetchBase: Ffetch<ffetchAddons.TimeoutAddon & ffetchAddons.QueryAddon & ffetchAddons.FormAddon & ffetchAddons.MultipartAddon, object>;
package/default.js ADDED
@@ -0,0 +1,18 @@
1
+ import { createFfetch } from "./ffetch.js";
2
+ import { timeout } from "./addons/timeout.js";
3
+ import { query } from "./addons/query.js";
4
+ import { form } from "./addons/form.js";
5
+ import { multipart } from "./addons/multipart.js";
6
+ const ffetchDefaultAddons = [
7
+ /* @__PURE__ */ timeout(),
8
+ /* @__PURE__ */ query(),
9
+ /* @__PURE__ */ form(),
10
+ /* @__PURE__ */ multipart()
11
+ ];
12
+ const ffetchBase = /* @__PURE__ */ createFfetch({
13
+ addons: ffetchDefaultAddons
14
+ });
15
+ export {
16
+ ffetchBase,
17
+ ffetchDefaultAddons
18
+ };