@fetchkit/ffetch 4.3.0 → 5.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/README.md +37 -18
- package/dist/index.cjs +118 -238
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -27
- package/dist/index.d.ts +13 -27
- package/dist/index.js +118 -239
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/plugins/circuit.cjs +122 -0
- package/dist/plugins/circuit.cjs.map +1 -0
- package/dist/plugins/circuit.d.cts +15 -0
- package/dist/plugins/circuit.d.ts +15 -0
- package/dist/plugins/circuit.js +85 -0
- package/dist/plugins/circuit.js.map +1 -0
- package/dist/plugins/dedupe.cjs +167 -0
- package/dist/plugins/dedupe.cjs.map +1 -0
- package/dist/plugins/dedupe.d.cts +22 -0
- package/dist/plugins/dedupe.d.ts +22 -0
- package/dist/plugins/dedupe.js +139 -0
- package/dist/plugins/dedupe.js.map +1 -0
- package/dist/plugins-9qcU31nU.d.cts +58 -0
- package/dist/plugins-9qcU31nU.d.ts +58 -0
- package/package.json +11 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/dedupe.ts","../../src/dedupeRequestHash.ts"],"sourcesContent":["import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\r\nimport {\r\n dedupeRequestHash,\r\n type DedupeHashParams,\r\n} from '../dedupeRequestHash.js'\r\n\r\ntype DedupeEntry = {\r\n promise: Promise<Response>\r\n resolve: (value: Response | PromiseLike<Response>) => void\r\n reject: (reason?: unknown) => void\r\n createdAt: number\r\n}\r\n\r\nexport type DedupePluginOptions = {\r\n hashFn?: (params: DedupeHashParams) => string | undefined\r\n ttl?: number\r\n sweepInterval?: number\r\n order?: number\r\n}\r\n\r\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\r\n return {\r\n method: ctx.request.method,\r\n url: ctx.request.url,\r\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\r\n headers: ctx.request.headers,\r\n signal:\r\n ctx.init.signal === undefined || ctx.init.signal === null\r\n ? undefined\r\n : ctx.init.signal,\r\n requestInit: ctx.init,\r\n request: ctx.request,\r\n }\r\n}\r\n\r\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\r\n const {\r\n hashFn = dedupeRequestHash,\r\n ttl,\r\n sweepInterval = 5000,\r\n order = 10,\r\n } = options\r\n\r\n const inFlight = new Map<string, DedupeEntry>()\r\n let sweeper: ReturnType<typeof setInterval> | undefined\r\n\r\n function startSweeper() {\r\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\r\n sweeper = setInterval(() => {\r\n const now = Date.now()\r\n for (const [key, entry] of inFlight.entries()) {\r\n if (now - entry.createdAt > ttl) {\r\n inFlight.delete(key)\r\n }\r\n }\r\n if (inFlight.size === 0 && sweeper) {\r\n clearInterval(sweeper)\r\n sweeper = undefined\r\n }\r\n }, sweepInterval)\r\n }\r\n\r\n function stopSweeperIfIdle() {\r\n if (inFlight.size === 0 && sweeper) {\r\n clearInterval(sweeper)\r\n sweeper = undefined\r\n }\r\n }\r\n\r\n return {\r\n name: 'dedupe',\r\n order,\r\n wrapDispatch: (next) => async (ctx) => {\r\n const key = hashFn(contextToHashParams(ctx))\r\n ctx.state.dedupeKey = key\r\n\r\n if (!key) {\r\n return next(ctx)\r\n }\r\n\r\n const existing = inFlight.get(key)\r\n if (existing) {\r\n return existing.promise\r\n }\r\n\r\n let settled = false\r\n let resolveFn: (value: Response | PromiseLike<Response>) => void\r\n let rejectFn: (reason?: unknown) => void\r\n\r\n const placeholder = new Promise<Response>((resolve, reject) => {\r\n resolveFn = (value) => {\r\n if (!settled) {\r\n settled = true\r\n resolve(value)\r\n }\r\n }\r\n rejectFn = (reason) => {\r\n if (!settled) {\r\n settled = true\r\n reject(reason)\r\n }\r\n }\r\n })\r\n // Internal placeholder can reject before a consumer attaches handlers.\r\n // Mark it observed to avoid unhandled-rejection noise.\r\n placeholder.catch(() => undefined)\r\n\r\n inFlight.set(key, {\r\n promise: placeholder,\r\n resolve: resolveFn!,\r\n reject: rejectFn!,\r\n createdAt: Date.now(),\r\n })\r\n startSweeper()\r\n\r\n const actualPromise = next(ctx)\r\n const entry = inFlight.get(key)\r\n if (entry) {\r\n actualPromise.then(\r\n (result) => entry.resolve(result),\r\n (error) => entry.reject(error)\r\n )\r\n inFlight.set(key, {\r\n ...entry,\r\n promise: actualPromise,\r\n })\r\n }\r\n\r\n return actualPromise.finally(() => {\r\n const current = inFlight.get(key)\r\n if (current?.promise === actualPromise) {\r\n inFlight.delete(key)\r\n stopSweeperIfIdle()\r\n }\r\n })\r\n },\r\n }\r\n}\r\n\r\nexport { dedupeRequestHash }\r\nexport type { DedupeHashParams }\r\n","export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { C as ClientPlugin } from '../plugins-9qcU31nU.cjs';
|
|
2
|
+
|
|
3
|
+
type DedupeHashParams = {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
body: string | FormData | URLSearchParams | Blob | ArrayBuffer | BufferSource | null | ReadableStream<unknown>;
|
|
7
|
+
headers?: Headers | Record<string, string>;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
requestInit?: RequestInit;
|
|
10
|
+
request?: Request;
|
|
11
|
+
};
|
|
12
|
+
declare function dedupeRequestHash(params: DedupeHashParams): string | undefined;
|
|
13
|
+
|
|
14
|
+
type DedupePluginOptions = {
|
|
15
|
+
hashFn?: (params: DedupeHashParams) => string | undefined;
|
|
16
|
+
ttl?: number;
|
|
17
|
+
sweepInterval?: number;
|
|
18
|
+
order?: number;
|
|
19
|
+
};
|
|
20
|
+
declare function dedupePlugin(options?: DedupePluginOptions): ClientPlugin;
|
|
21
|
+
|
|
22
|
+
export { type DedupeHashParams, type DedupePluginOptions, dedupePlugin, dedupeRequestHash };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { C as ClientPlugin } from '../plugins-9qcU31nU.js';
|
|
2
|
+
|
|
3
|
+
type DedupeHashParams = {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
body: string | FormData | URLSearchParams | Blob | ArrayBuffer | BufferSource | null | ReadableStream<unknown>;
|
|
7
|
+
headers?: Headers | Record<string, string>;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
requestInit?: RequestInit;
|
|
10
|
+
request?: Request;
|
|
11
|
+
};
|
|
12
|
+
declare function dedupeRequestHash(params: DedupeHashParams): string | undefined;
|
|
13
|
+
|
|
14
|
+
type DedupePluginOptions = {
|
|
15
|
+
hashFn?: (params: DedupeHashParams) => string | undefined;
|
|
16
|
+
ttl?: number;
|
|
17
|
+
sweepInterval?: number;
|
|
18
|
+
order?: number;
|
|
19
|
+
};
|
|
20
|
+
declare function dedupePlugin(options?: DedupePluginOptions): ClientPlugin;
|
|
21
|
+
|
|
22
|
+
export { type DedupeHashParams, type DedupePluginOptions, dedupePlugin, dedupeRequestHash };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/dedupeRequestHash.ts
|
|
2
|
+
function dedupeRequestHash(params) {
|
|
3
|
+
const { method, url, body } = params;
|
|
4
|
+
let bodyString = "";
|
|
5
|
+
if (body instanceof FormData) {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) {
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
if (typeof body === "string") {
|
|
12
|
+
bodyString = body;
|
|
13
|
+
} else if (body instanceof URLSearchParams) {
|
|
14
|
+
bodyString = body.toString();
|
|
15
|
+
} else if (body instanceof ArrayBuffer) {
|
|
16
|
+
bodyString = Buffer.from(body).toString("base64");
|
|
17
|
+
} else if (body instanceof Uint8Array) {
|
|
18
|
+
bodyString = Buffer.from(body).toString("base64");
|
|
19
|
+
} else if (body instanceof Blob) {
|
|
20
|
+
bodyString = `[blob:${body.type}:${body.size}]`;
|
|
21
|
+
} else if (body == null) {
|
|
22
|
+
bodyString = "";
|
|
23
|
+
} else {
|
|
24
|
+
try {
|
|
25
|
+
bodyString = JSON.stringify(body);
|
|
26
|
+
} catch {
|
|
27
|
+
bodyString = "[unserializable-body]";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return `${method.toUpperCase()}|${url}|${bodyString}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/plugins/dedupe.ts
|
|
34
|
+
function contextToHashParams(ctx) {
|
|
35
|
+
return {
|
|
36
|
+
method: ctx.request.method,
|
|
37
|
+
url: ctx.request.url,
|
|
38
|
+
body: ctx.init.body ?? null,
|
|
39
|
+
headers: ctx.request.headers,
|
|
40
|
+
signal: ctx.init.signal === void 0 || ctx.init.signal === null ? void 0 : ctx.init.signal,
|
|
41
|
+
requestInit: ctx.init,
|
|
42
|
+
request: ctx.request
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function dedupePlugin(options = {}) {
|
|
46
|
+
const {
|
|
47
|
+
hashFn = dedupeRequestHash,
|
|
48
|
+
ttl,
|
|
49
|
+
sweepInterval = 5e3,
|
|
50
|
+
order = 10
|
|
51
|
+
} = options;
|
|
52
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
53
|
+
let sweeper;
|
|
54
|
+
function startSweeper() {
|
|
55
|
+
if (sweeper || typeof ttl !== "number" || ttl <= 0) return;
|
|
56
|
+
sweeper = setInterval(() => {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
for (const [key, entry] of inFlight.entries()) {
|
|
59
|
+
if (now - entry.createdAt > ttl) {
|
|
60
|
+
inFlight.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (inFlight.size === 0 && sweeper) {
|
|
64
|
+
clearInterval(sweeper);
|
|
65
|
+
sweeper = void 0;
|
|
66
|
+
}
|
|
67
|
+
}, sweepInterval);
|
|
68
|
+
}
|
|
69
|
+
function stopSweeperIfIdle() {
|
|
70
|
+
if (inFlight.size === 0 && sweeper) {
|
|
71
|
+
clearInterval(sweeper);
|
|
72
|
+
sweeper = void 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
name: "dedupe",
|
|
77
|
+
order,
|
|
78
|
+
wrapDispatch: (next) => async (ctx) => {
|
|
79
|
+
const key = hashFn(contextToHashParams(ctx));
|
|
80
|
+
ctx.state.dedupeKey = key;
|
|
81
|
+
if (!key) {
|
|
82
|
+
return next(ctx);
|
|
83
|
+
}
|
|
84
|
+
const existing = inFlight.get(key);
|
|
85
|
+
if (existing) {
|
|
86
|
+
return existing.promise;
|
|
87
|
+
}
|
|
88
|
+
let settled = false;
|
|
89
|
+
let resolveFn;
|
|
90
|
+
let rejectFn;
|
|
91
|
+
const placeholder = new Promise((resolve, reject) => {
|
|
92
|
+
resolveFn = (value) => {
|
|
93
|
+
if (!settled) {
|
|
94
|
+
settled = true;
|
|
95
|
+
resolve(value);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
rejectFn = (reason) => {
|
|
99
|
+
if (!settled) {
|
|
100
|
+
settled = true;
|
|
101
|
+
reject(reason);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
placeholder.catch(() => void 0);
|
|
106
|
+
inFlight.set(key, {
|
|
107
|
+
promise: placeholder,
|
|
108
|
+
resolve: resolveFn,
|
|
109
|
+
reject: rejectFn,
|
|
110
|
+
createdAt: Date.now()
|
|
111
|
+
});
|
|
112
|
+
startSweeper();
|
|
113
|
+
const actualPromise = next(ctx);
|
|
114
|
+
const entry = inFlight.get(key);
|
|
115
|
+
if (entry) {
|
|
116
|
+
actualPromise.then(
|
|
117
|
+
(result) => entry.resolve(result),
|
|
118
|
+
(error) => entry.reject(error)
|
|
119
|
+
);
|
|
120
|
+
inFlight.set(key, {
|
|
121
|
+
...entry,
|
|
122
|
+
promise: actualPromise
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return actualPromise.finally(() => {
|
|
126
|
+
const current = inFlight.get(key);
|
|
127
|
+
if (current?.promise === actualPromise) {
|
|
128
|
+
inFlight.delete(key);
|
|
129
|
+
stopSweeperIfIdle();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
dedupePlugin,
|
|
137
|
+
dedupeRequestHash
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=dedupe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/dedupeRequestHash.ts","../../src/plugins/dedupe.ts"],"sourcesContent":["export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n","import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\r\nimport {\r\n dedupeRequestHash,\r\n type DedupeHashParams,\r\n} from '../dedupeRequestHash.js'\r\n\r\ntype DedupeEntry = {\r\n promise: Promise<Response>\r\n resolve: (value: Response | PromiseLike<Response>) => void\r\n reject: (reason?: unknown) => void\r\n createdAt: number\r\n}\r\n\r\nexport type DedupePluginOptions = {\r\n hashFn?: (params: DedupeHashParams) => string | undefined\r\n ttl?: number\r\n sweepInterval?: number\r\n order?: number\r\n}\r\n\r\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\r\n return {\r\n method: ctx.request.method,\r\n url: ctx.request.url,\r\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\r\n headers: ctx.request.headers,\r\n signal:\r\n ctx.init.signal === undefined || ctx.init.signal === null\r\n ? undefined\r\n : ctx.init.signal,\r\n requestInit: ctx.init,\r\n request: ctx.request,\r\n }\r\n}\r\n\r\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\r\n const {\r\n hashFn = dedupeRequestHash,\r\n ttl,\r\n sweepInterval = 5000,\r\n order = 10,\r\n } = options\r\n\r\n const inFlight = new Map<string, DedupeEntry>()\r\n let sweeper: ReturnType<typeof setInterval> | undefined\r\n\r\n function startSweeper() {\r\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\r\n sweeper = setInterval(() => {\r\n const now = Date.now()\r\n for (const [key, entry] of inFlight.entries()) {\r\n if (now - entry.createdAt > ttl) {\r\n inFlight.delete(key)\r\n }\r\n }\r\n if (inFlight.size === 0 && sweeper) {\r\n clearInterval(sweeper)\r\n sweeper = undefined\r\n }\r\n }, sweepInterval)\r\n }\r\n\r\n function stopSweeperIfIdle() {\r\n if (inFlight.size === 0 && sweeper) {\r\n clearInterval(sweeper)\r\n sweeper = undefined\r\n }\r\n }\r\n\r\n return {\r\n name: 'dedupe',\r\n order,\r\n wrapDispatch: (next) => async (ctx) => {\r\n const key = hashFn(contextToHashParams(ctx))\r\n ctx.state.dedupeKey = key\r\n\r\n if (!key) {\r\n return next(ctx)\r\n }\r\n\r\n const existing = inFlight.get(key)\r\n if (existing) {\r\n return existing.promise\r\n }\r\n\r\n let settled = false\r\n let resolveFn: (value: Response | PromiseLike<Response>) => void\r\n let rejectFn: (reason?: unknown) => void\r\n\r\n const placeholder = new Promise<Response>((resolve, reject) => {\r\n resolveFn = (value) => {\r\n if (!settled) {\r\n settled = true\r\n resolve(value)\r\n }\r\n }\r\n rejectFn = (reason) => {\r\n if (!settled) {\r\n settled = true\r\n reject(reason)\r\n }\r\n }\r\n })\r\n // Internal placeholder can reject before a consumer attaches handlers.\r\n // Mark it observed to avoid unhandled-rejection noise.\r\n placeholder.catch(() => undefined)\r\n\r\n inFlight.set(key, {\r\n promise: placeholder,\r\n resolve: resolveFn!,\r\n reject: rejectFn!,\r\n createdAt: Date.now(),\r\n })\r\n startSweeper()\r\n\r\n const actualPromise = next(ctx)\r\n const entry = inFlight.get(key)\r\n if (entry) {\r\n actualPromise.then(\r\n (result) => entry.resolve(result),\r\n (error) => entry.reject(error)\r\n )\r\n inFlight.set(key, {\r\n ...entry,\r\n promise: actualPromise,\r\n })\r\n }\r\n\r\n return actualPromise.finally(() => {\r\n const current = inFlight.get(key)\r\n if (current?.promise === actualPromise) {\r\n inFlight.delete(key)\r\n stopSweeperIfIdle()\r\n }\r\n })\r\n },\r\n }\r\n}\r\n\r\nexport { dedupeRequestHash }\r\nexport type { DedupeHashParams }\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type PluginState = Record<string, unknown>;
|
|
2
|
+
type PluginExtensionBase = Record<PropertyKey, unknown>;
|
|
3
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
|
|
4
|
+
type PluginExtensionOf<P> = P extends ClientPlugin<infer TExtension> ? TExtension : Record<never, never>;
|
|
5
|
+
type PluginExtensions<TPlugins extends readonly ClientPlugin<PluginExtensionBase>[]> = Extract<UnionToIntersection<PluginExtensionOf<TPlugins[number]>>, object>;
|
|
6
|
+
type PluginSetupContext<TExtension extends PluginExtensionBase = Record<never, never>> = {
|
|
7
|
+
defineExtension: <K extends keyof TExtension>(key: K, descriptor: {
|
|
8
|
+
value: TExtension[K];
|
|
9
|
+
enumerable?: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
get: () => TExtension[K];
|
|
12
|
+
enumerable?: boolean;
|
|
13
|
+
}) => void;
|
|
14
|
+
};
|
|
15
|
+
type PluginSignalMetadata = {
|
|
16
|
+
user?: AbortSignal;
|
|
17
|
+
transformed?: AbortSignal;
|
|
18
|
+
timeout?: AbortSignal;
|
|
19
|
+
combined?: AbortSignal;
|
|
20
|
+
};
|
|
21
|
+
type PluginRetryMetadata = {
|
|
22
|
+
configuredRetries: number;
|
|
23
|
+
configuredDelay: number | ((ctx: {
|
|
24
|
+
attempt: number;
|
|
25
|
+
request: Request;
|
|
26
|
+
response?: Response;
|
|
27
|
+
error?: unknown;
|
|
28
|
+
}) => number);
|
|
29
|
+
attempt: number;
|
|
30
|
+
shouldRetryResult?: boolean;
|
|
31
|
+
lastError?: unknown;
|
|
32
|
+
lastResponse?: Response;
|
|
33
|
+
};
|
|
34
|
+
type PluginRequestMetadata = {
|
|
35
|
+
startedAt: number;
|
|
36
|
+
timeoutMs: number;
|
|
37
|
+
signals: PluginSignalMetadata;
|
|
38
|
+
retry: PluginRetryMetadata;
|
|
39
|
+
};
|
|
40
|
+
type PluginRequestContext = {
|
|
41
|
+
request: Request;
|
|
42
|
+
init: RequestInit;
|
|
43
|
+
state: PluginState;
|
|
44
|
+
metadata: PluginRequestMetadata;
|
|
45
|
+
};
|
|
46
|
+
type PluginDispatch = (ctx: PluginRequestContext) => Promise<Response>;
|
|
47
|
+
type ClientPlugin<TExtension extends PluginExtensionBase = Record<never, never>> = {
|
|
48
|
+
name: string;
|
|
49
|
+
order?: number;
|
|
50
|
+
setup?: (ctx: PluginSetupContext<TExtension>) => void;
|
|
51
|
+
preRequest?: (ctx: PluginRequestContext) => void | Promise<void>;
|
|
52
|
+
wrapDispatch?: (next: PluginDispatch) => PluginDispatch;
|
|
53
|
+
onSuccess?: (ctx: PluginRequestContext, response: Response) => void | Promise<void>;
|
|
54
|
+
onError?: (ctx: PluginRequestContext, error: unknown) => void | Promise<void>;
|
|
55
|
+
onFinally?: (ctx: PluginRequestContext) => void | Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type { ClientPlugin as C, PluginExtensionBase as P, PluginExtensions as a, PluginRequestContext as b, PluginDispatch as c, PluginSetupContext as d };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type PluginState = Record<string, unknown>;
|
|
2
|
+
type PluginExtensionBase = Record<PropertyKey, unknown>;
|
|
3
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
|
|
4
|
+
type PluginExtensionOf<P> = P extends ClientPlugin<infer TExtension> ? TExtension : Record<never, never>;
|
|
5
|
+
type PluginExtensions<TPlugins extends readonly ClientPlugin<PluginExtensionBase>[]> = Extract<UnionToIntersection<PluginExtensionOf<TPlugins[number]>>, object>;
|
|
6
|
+
type PluginSetupContext<TExtension extends PluginExtensionBase = Record<never, never>> = {
|
|
7
|
+
defineExtension: <K extends keyof TExtension>(key: K, descriptor: {
|
|
8
|
+
value: TExtension[K];
|
|
9
|
+
enumerable?: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
get: () => TExtension[K];
|
|
12
|
+
enumerable?: boolean;
|
|
13
|
+
}) => void;
|
|
14
|
+
};
|
|
15
|
+
type PluginSignalMetadata = {
|
|
16
|
+
user?: AbortSignal;
|
|
17
|
+
transformed?: AbortSignal;
|
|
18
|
+
timeout?: AbortSignal;
|
|
19
|
+
combined?: AbortSignal;
|
|
20
|
+
};
|
|
21
|
+
type PluginRetryMetadata = {
|
|
22
|
+
configuredRetries: number;
|
|
23
|
+
configuredDelay: number | ((ctx: {
|
|
24
|
+
attempt: number;
|
|
25
|
+
request: Request;
|
|
26
|
+
response?: Response;
|
|
27
|
+
error?: unknown;
|
|
28
|
+
}) => number);
|
|
29
|
+
attempt: number;
|
|
30
|
+
shouldRetryResult?: boolean;
|
|
31
|
+
lastError?: unknown;
|
|
32
|
+
lastResponse?: Response;
|
|
33
|
+
};
|
|
34
|
+
type PluginRequestMetadata = {
|
|
35
|
+
startedAt: number;
|
|
36
|
+
timeoutMs: number;
|
|
37
|
+
signals: PluginSignalMetadata;
|
|
38
|
+
retry: PluginRetryMetadata;
|
|
39
|
+
};
|
|
40
|
+
type PluginRequestContext = {
|
|
41
|
+
request: Request;
|
|
42
|
+
init: RequestInit;
|
|
43
|
+
state: PluginState;
|
|
44
|
+
metadata: PluginRequestMetadata;
|
|
45
|
+
};
|
|
46
|
+
type PluginDispatch = (ctx: PluginRequestContext) => Promise<Response>;
|
|
47
|
+
type ClientPlugin<TExtension extends PluginExtensionBase = Record<never, never>> = {
|
|
48
|
+
name: string;
|
|
49
|
+
order?: number;
|
|
50
|
+
setup?: (ctx: PluginSetupContext<TExtension>) => void;
|
|
51
|
+
preRequest?: (ctx: PluginRequestContext) => void | Promise<void>;
|
|
52
|
+
wrapDispatch?: (next: PluginDispatch) => PluginDispatch;
|
|
53
|
+
onSuccess?: (ctx: PluginRequestContext, response: Response) => void | Promise<void>;
|
|
54
|
+
onError?: (ctx: PluginRequestContext, error: unknown) => void | Promise<void>;
|
|
55
|
+
onFinally?: (ctx: PluginRequestContext) => void | Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type { ClientPlugin as C, PluginExtensionBase as P, PluginExtensions as a, PluginRequestContext as b, PluginDispatch as c, PluginSetupContext as d };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fetchkit/ffetch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Fetch wrapper with configurable timeouts, retries, and TypeScript-first DX",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"import": "./dist/index.js",
|
|
28
28
|
"require": "./dist/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./plugins/dedupe": {
|
|
31
|
+
"types": "./dist/plugins/dedupe.d.ts",
|
|
32
|
+
"import": "./dist/plugins/dedupe.js",
|
|
33
|
+
"require": "./dist/plugins/dedupe.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./plugins/circuit": {
|
|
36
|
+
"types": "./dist/plugins/circuit.d.ts",
|
|
37
|
+
"import": "./dist/plugins/circuit.js",
|
|
38
|
+
"require": "./dist/plugins/circuit.cjs"
|
|
29
39
|
}
|
|
30
40
|
},
|
|
31
41
|
"files": [
|