@fetchkit/ffetch 5.5.3 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.cjs +63 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +63 -46
- 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/bulkhead.cjs +25 -13
- package/dist/plugins/bulkhead.cjs.map +1 -1
- package/dist/plugins/bulkhead.js +22 -14
- package/dist/plugins/bulkhead.js.map +1 -1
- package/dist/plugins/dedupe.cjs +53 -4
- package/dist/plugins/dedupe.cjs.map +1 -1
- package/dist/plugins/dedupe.js +39 -4
- package/dist/plugins/dedupe.js.map +1 -1
- package/dist/plugins/download-progress.cjs +3 -2
- package/dist/plugins/download-progress.cjs.map +1 -1
- package/dist/plugins/download-progress.js +3 -2
- package/dist/plugins/download-progress.js.map +1 -1
- package/dist/plugins/hedge.cjs +22 -22
- package/dist/plugins/hedge.cjs.map +1 -1
- package/dist/plugins/hedge.js +22 -22
- package/dist/plugins/hedge.js.map +1 -1
- package/package.json +2 -2
package/dist/plugins/dedupe.cjs
CHANGED
|
@@ -25,6 +25,25 @@ __export(dedupe_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(dedupe_exports);
|
|
27
27
|
|
|
28
|
+
// src/error.ts
|
|
29
|
+
var BaseError = class extends Error {
|
|
30
|
+
constructor(name, message, cause) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = name;
|
|
33
|
+
if (cause !== void 0) this.cause = cause;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var TimeoutError = class extends BaseError {
|
|
37
|
+
constructor(message = "Request timed out", cause) {
|
|
38
|
+
super("TimeoutError", message, cause);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var AbortError = class extends BaseError {
|
|
42
|
+
constructor(message = "Request was aborted", cause) {
|
|
43
|
+
super("AbortError", message, cause);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
// src/dedupeRequestHash.ts
|
|
29
48
|
function toBase64(bytes) {
|
|
30
49
|
const maybeBuffer = globalThis.Buffer;
|
|
@@ -76,7 +95,7 @@ function contextToHashParams(ctx) {
|
|
|
76
95
|
return {
|
|
77
96
|
method: ctx.request.method,
|
|
78
97
|
url: ctx.request.url,
|
|
79
|
-
body: ctx.init.body ??
|
|
98
|
+
body: ctx.init.body ?? ctx.request.body,
|
|
80
99
|
headers: ctx.request.headers,
|
|
81
100
|
signal: ctx.init.signal === void 0 || ctx.init.signal === null ? void 0 : ctx.init.signal,
|
|
82
101
|
requestInit: ctx.init,
|
|
@@ -92,6 +111,17 @@ function dedupePlugin(options = {}) {
|
|
|
92
111
|
} = options;
|
|
93
112
|
const inFlight = /* @__PURE__ */ new Map();
|
|
94
113
|
let sweeper;
|
|
114
|
+
function makeCancellationError(ctx) {
|
|
115
|
+
const { signals } = ctx.metadata;
|
|
116
|
+
if (signals.user?.aborted) {
|
|
117
|
+
return new AbortError("Request was aborted by user", signals.user.reason);
|
|
118
|
+
}
|
|
119
|
+
if (signals.timeout?.aborted) {
|
|
120
|
+
return new TimeoutError("signal timed out", signals.timeout.reason);
|
|
121
|
+
}
|
|
122
|
+
const signal = signals.combined ?? ctx.request.signal;
|
|
123
|
+
return new AbortError("Request was aborted", signal.reason);
|
|
124
|
+
}
|
|
95
125
|
function startSweeper() {
|
|
96
126
|
if (sweeper || typeof ttl !== "number" || ttl <= 0) return;
|
|
97
127
|
sweeper = setInterval(() => {
|
|
@@ -125,7 +155,24 @@ function dedupePlugin(options = {}) {
|
|
|
125
155
|
const existing = inFlight.get(key);
|
|
126
156
|
if (existing) {
|
|
127
157
|
return new Promise((resolve, reject) => {
|
|
128
|
-
|
|
158
|
+
const signal = ctx.metadata.signals.combined ?? ctx.request.signal;
|
|
159
|
+
const waiter = { resolve, reject, cleanup: () => {
|
|
160
|
+
} };
|
|
161
|
+
const onAbort = () => {
|
|
162
|
+
const index = existing.waiters.indexOf(waiter);
|
|
163
|
+
existing.waiters.splice(index, 1);
|
|
164
|
+
waiter.cleanup();
|
|
165
|
+
reject(makeCancellationError(ctx));
|
|
166
|
+
};
|
|
167
|
+
waiter.cleanup = () => {
|
|
168
|
+
signal.removeEventListener("abort", onAbort);
|
|
169
|
+
};
|
|
170
|
+
if (signal.aborted) {
|
|
171
|
+
reject(makeCancellationError(ctx));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
175
|
+
existing.waiters.push(waiter);
|
|
129
176
|
});
|
|
130
177
|
}
|
|
131
178
|
const waiters = [];
|
|
@@ -138,12 +185,14 @@ function dedupePlugin(options = {}) {
|
|
|
138
185
|
startSweeper();
|
|
139
186
|
actualPromise.then(
|
|
140
187
|
(result) => {
|
|
141
|
-
for (const waiter of waiters) {
|
|
188
|
+
for (const waiter of waiters.splice(0)) {
|
|
189
|
+
waiter.cleanup();
|
|
142
190
|
waiter.resolve(result.clone());
|
|
143
191
|
}
|
|
144
192
|
},
|
|
145
193
|
(error) => {
|
|
146
|
-
for (const waiter of waiters) {
|
|
194
|
+
for (const waiter of waiters.splice(0)) {
|
|
195
|
+
waiter.cleanup();
|
|
147
196
|
waiter.reject(error);
|
|
148
197
|
}
|
|
149
198
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/dedupe.ts","../../src/dedupeRequestHash.ts"],"sourcesContent":["import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n existing.waiters.push({ resolve, reject })\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters) {\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters) {\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\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\nfunction toBase64(bytes: Uint8Array): string {\n // Use Node Buffer when available, otherwise fall back to btoa for browser-like runtimes.\n const maybeBuffer = (\n globalThis as {\n Buffer?: {\n from(input: Uint8Array): { toString(encoding: string): string }\n }\n }\n ).Buffer\n if (maybeBuffer) {\n return maybeBuffer.from(bytes).toString('base64')\n }\n\n let binary = ''\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n if (typeof btoa === 'function') {\n return btoa(binary)\n }\n\n throw new Error('Base64 encoding is not available in this runtime')\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString: string\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 = toBase64(new Uint8Array(body))\n } else if (body instanceof Uint8Array) {\n bodyString = toBase64(body)\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/dedupe.ts","../../src/error.ts","../../src/dedupeRequestHash.ts"],"sourcesContent":["import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport { AbortError, TimeoutError } from '../error.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n cleanup: () => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? ctx.request.body) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function makeCancellationError(ctx: PluginRequestContext): Error {\n const { signals } = ctx.metadata\n if (signals.user?.aborted) {\n return new AbortError('Request was aborted by user', signals.user.reason)\n }\n if (signals.timeout?.aborted) {\n return new TimeoutError('signal timed out', signals.timeout.reason)\n }\n const signal = signals.combined ?? ctx.request.signal\n return new AbortError('Request was aborted', signal.reason)\n }\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n const signal = ctx.metadata.signals.combined ?? ctx.request.signal\n const waiter: Waiter = { resolve, reject, cleanup: () => {} }\n const onAbort = () => {\n const index = existing.waiters.indexOf(waiter)\n existing.waiters.splice(index, 1)\n waiter.cleanup()\n reject(makeCancellationError(ctx))\n }\n\n waiter.cleanup = () => {\n signal.removeEventListener('abort', onAbort)\n }\n\n if (signal.aborted) {\n reject(makeCancellationError(ctx))\n return\n }\n\n signal.addEventListener('abort', onAbort, { once: true })\n existing.waiters.push(waiter)\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters.splice(0)) {\n waiter.cleanup()\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters.splice(0)) {\n waiter.cleanup()\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n","// Base error class to reduce duplication\nclass BaseError extends Error {\n public cause?: unknown\n constructor(name: string, message: string, cause?: unknown) {\n super(message)\n this.name = name\n if (cause !== undefined) this.cause = cause\n }\n}\n\nexport class TimeoutError extends BaseError {\n constructor(message = 'Request timed out', cause?: unknown) {\n super('TimeoutError', message, cause)\n }\n}\n\nexport class CircuitOpenError extends BaseError {\n constructor(message = 'Circuit is open', cause?: unknown) {\n super('CircuitOpenError', message, cause)\n }\n}\n\nexport class BulkheadFullError extends BaseError {\n constructor(message = 'Bulkhead queue is full', cause?: unknown) {\n super('BulkheadFullError', message, cause)\n }\n}\n\nexport class AbortError extends BaseError {\n constructor(message = 'Request was aborted', cause?: unknown) {\n super('AbortError', message, cause)\n }\n}\n\nexport class RetryLimitError extends BaseError {\n constructor(message = 'Retry limit reached', cause?: unknown) {\n super('RetryLimitError', message, cause)\n }\n}\n\nexport class NetworkError extends BaseError {\n constructor(message = 'Network error occurred', cause?: unknown) {\n super('NetworkError', message, cause)\n }\n}\n\nexport class HttpError extends BaseError {\n constructor(message = 'HTTP error occurred', cause?: unknown) {\n super('HttpError', message, cause)\n }\n}\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\nfunction toBase64(bytes: Uint8Array): string {\n // Use Node Buffer when available, otherwise fall back to btoa for browser-like runtimes.\n const maybeBuffer = (\n globalThis as {\n Buffer?: {\n from(input: Uint8Array): { toString(encoding: string): string }\n }\n }\n ).Buffer\n if (maybeBuffer) {\n return maybeBuffer.from(bytes).toString('base64')\n }\n\n let binary = ''\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n if (typeof btoa === 'function') {\n return btoa(binary)\n }\n\n throw new Error('Base64 encoding is not available in this runtime')\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString: string\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 = toBase64(new Uint8Array(body))\n } else if (body instanceof Uint8Array) {\n bodyString = toBase64(body)\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":[]}
|
package/dist/plugins/dedupe.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbortError,
|
|
3
|
+
TimeoutError
|
|
4
|
+
} from "../chunk-ZIJ2DZYZ.js";
|
|
5
|
+
|
|
1
6
|
// src/dedupeRequestHash.ts
|
|
2
7
|
function toBase64(bytes) {
|
|
3
8
|
const maybeBuffer = globalThis.Buffer;
|
|
@@ -49,7 +54,7 @@ function contextToHashParams(ctx) {
|
|
|
49
54
|
return {
|
|
50
55
|
method: ctx.request.method,
|
|
51
56
|
url: ctx.request.url,
|
|
52
|
-
body: ctx.init.body ??
|
|
57
|
+
body: ctx.init.body ?? ctx.request.body,
|
|
53
58
|
headers: ctx.request.headers,
|
|
54
59
|
signal: ctx.init.signal === void 0 || ctx.init.signal === null ? void 0 : ctx.init.signal,
|
|
55
60
|
requestInit: ctx.init,
|
|
@@ -65,6 +70,17 @@ function dedupePlugin(options = {}) {
|
|
|
65
70
|
} = options;
|
|
66
71
|
const inFlight = /* @__PURE__ */ new Map();
|
|
67
72
|
let sweeper;
|
|
73
|
+
function makeCancellationError(ctx) {
|
|
74
|
+
const { signals } = ctx.metadata;
|
|
75
|
+
if (signals.user?.aborted) {
|
|
76
|
+
return new AbortError("Request was aborted by user", signals.user.reason);
|
|
77
|
+
}
|
|
78
|
+
if (signals.timeout?.aborted) {
|
|
79
|
+
return new TimeoutError("signal timed out", signals.timeout.reason);
|
|
80
|
+
}
|
|
81
|
+
const signal = signals.combined ?? ctx.request.signal;
|
|
82
|
+
return new AbortError("Request was aborted", signal.reason);
|
|
83
|
+
}
|
|
68
84
|
function startSweeper() {
|
|
69
85
|
if (sweeper || typeof ttl !== "number" || ttl <= 0) return;
|
|
70
86
|
sweeper = setInterval(() => {
|
|
@@ -98,7 +114,24 @@ function dedupePlugin(options = {}) {
|
|
|
98
114
|
const existing = inFlight.get(key);
|
|
99
115
|
if (existing) {
|
|
100
116
|
return new Promise((resolve, reject) => {
|
|
101
|
-
|
|
117
|
+
const signal = ctx.metadata.signals.combined ?? ctx.request.signal;
|
|
118
|
+
const waiter = { resolve, reject, cleanup: () => {
|
|
119
|
+
} };
|
|
120
|
+
const onAbort = () => {
|
|
121
|
+
const index = existing.waiters.indexOf(waiter);
|
|
122
|
+
existing.waiters.splice(index, 1);
|
|
123
|
+
waiter.cleanup();
|
|
124
|
+
reject(makeCancellationError(ctx));
|
|
125
|
+
};
|
|
126
|
+
waiter.cleanup = () => {
|
|
127
|
+
signal.removeEventListener("abort", onAbort);
|
|
128
|
+
};
|
|
129
|
+
if (signal.aborted) {
|
|
130
|
+
reject(makeCancellationError(ctx));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
134
|
+
existing.waiters.push(waiter);
|
|
102
135
|
});
|
|
103
136
|
}
|
|
104
137
|
const waiters = [];
|
|
@@ -111,12 +144,14 @@ function dedupePlugin(options = {}) {
|
|
|
111
144
|
startSweeper();
|
|
112
145
|
actualPromise.then(
|
|
113
146
|
(result) => {
|
|
114
|
-
for (const waiter of waiters) {
|
|
147
|
+
for (const waiter of waiters.splice(0)) {
|
|
148
|
+
waiter.cleanup();
|
|
115
149
|
waiter.resolve(result.clone());
|
|
116
150
|
}
|
|
117
151
|
},
|
|
118
152
|
(error) => {
|
|
119
|
-
for (const waiter of waiters) {
|
|
153
|
+
for (const waiter of waiters.splice(0)) {
|
|
154
|
+
waiter.cleanup();
|
|
120
155
|
waiter.reject(error);
|
|
121
156
|
}
|
|
122
157
|
}
|
|
@@ -1 +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\nfunction toBase64(bytes: Uint8Array): string {\n // Use Node Buffer when available, otherwise fall back to btoa for browser-like runtimes.\n const maybeBuffer = (\n globalThis as {\n Buffer?: {\n from(input: Uint8Array): { toString(encoding: string): string }\n }\n }\n ).Buffer\n if (maybeBuffer) {\n return maybeBuffer.from(bytes).toString('base64')\n }\n\n let binary = ''\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n if (typeof btoa === 'function') {\n return btoa(binary)\n }\n\n throw new Error('Base64 encoding is not available in this runtime')\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString: string\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 = toBase64(new Uint8Array(body))\n } else if (body instanceof Uint8Array) {\n bodyString = toBase64(body)\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'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n existing.waiters.push({ resolve, reject })\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters) {\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters) {\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n"],"mappings":"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","names":[]}
|
|
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\nfunction toBase64(bytes: Uint8Array): string {\n // Use Node Buffer when available, otherwise fall back to btoa for browser-like runtimes.\n const maybeBuffer = (\n globalThis as {\n Buffer?: {\n from(input: Uint8Array): { toString(encoding: string): string }\n }\n }\n ).Buffer\n if (maybeBuffer) {\n return maybeBuffer.from(bytes).toString('base64')\n }\n\n let binary = ''\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n if (typeof btoa === 'function') {\n return btoa(binary)\n }\n\n throw new Error('Base64 encoding is not available in this runtime')\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString: string\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 = toBase64(new Uint8Array(body))\n } else if (body instanceof Uint8Array) {\n bodyString = toBase64(body)\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'\nimport { AbortError, TimeoutError } from '../error.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n cleanup: () => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? ctx.request.body) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function makeCancellationError(ctx: PluginRequestContext): Error {\n const { signals } = ctx.metadata\n if (signals.user?.aborted) {\n return new AbortError('Request was aborted by user', signals.user.reason)\n }\n if (signals.timeout?.aborted) {\n return new TimeoutError('signal timed out', signals.timeout.reason)\n }\n const signal = signals.combined ?? ctx.request.signal\n return new AbortError('Request was aborted', signal.reason)\n }\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n const signal = ctx.metadata.signals.combined ?? ctx.request.signal\n const waiter: Waiter = { resolve, reject, cleanup: () => {} }\n const onAbort = () => {\n const index = existing.waiters.indexOf(waiter)\n existing.waiters.splice(index, 1)\n waiter.cleanup()\n reject(makeCancellationError(ctx))\n }\n\n waiter.cleanup = () => {\n signal.removeEventListener('abort', onAbort)\n }\n\n if (signal.aborted) {\n reject(makeCancellationError(ctx))\n return\n }\n\n signal.addEventListener('abort', onAbort, { once: true })\n existing.waiters.push(waiter)\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters.splice(0)) {\n waiter.cleanup()\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters.splice(0)) {\n waiter.cleanup()\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n"],"mappings":"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","names":[]}
|
|
@@ -32,13 +32,14 @@ function downloadProgressPlugin(onProgress) {
|
|
|
32
32
|
return response;
|
|
33
33
|
}
|
|
34
34
|
const contentLength = response.headers.get("content-length");
|
|
35
|
-
const
|
|
35
|
+
const parsedContentLength = contentLength && /^\d+$/.test(contentLength) ? Number(contentLength) : 0;
|
|
36
|
+
const totalBytes = Number.isSafeInteger(parsedContentLength) ? parsedContentLength : 0;
|
|
36
37
|
let transferredBytes = 0;
|
|
37
38
|
const stream = response.body.pipeThrough(
|
|
38
39
|
new TransformStream({
|
|
39
40
|
transform(chunk, controller) {
|
|
40
41
|
transferredBytes += chunk.byteLength;
|
|
41
|
-
const percent = totalBytes > 0 ? transferredBytes / totalBytes : 0;
|
|
42
|
+
const percent = totalBytes > 0 ? Math.min(transferredBytes / totalBytes, 1) : 0;
|
|
42
43
|
onProgress({ percent, transferredBytes, totalBytes }, chunk);
|
|
43
44
|
controller.enqueue(chunk);
|
|
44
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/download-progress.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nexport type DownloadProgressEvent = {\n percent: number\n transferredBytes: number\n totalBytes: number\n}\n\nexport type DownloadProgressCallback = (\n progress: DownloadProgressEvent,\n chunk: Uint8Array\n) => void\n\nexport function downloadProgressPlugin(\n onProgress: DownloadProgressCallback\n): ClientPlugin {\n return {\n name: 'downloadProgress',\n wrapDispatch: (next) => async (ctx) => {\n const response = await next(ctx)\n\n if (!response.body) {\n return response\n }\n\n const contentLength = response.headers.get('content-length')\n const
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/download-progress.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nexport type DownloadProgressEvent = {\n percent: number\n transferredBytes: number\n totalBytes: number\n}\n\nexport type DownloadProgressCallback = (\n progress: DownloadProgressEvent,\n chunk: Uint8Array\n) => void\n\nexport function downloadProgressPlugin(\n onProgress: DownloadProgressCallback\n): ClientPlugin {\n return {\n name: 'downloadProgress',\n wrapDispatch: (next) => async (ctx) => {\n const response = await next(ctx)\n\n if (!response.body) {\n return response\n }\n\n const contentLength = response.headers.get('content-length')\n const parsedContentLength =\n contentLength && /^\\d+$/.test(contentLength) ? Number(contentLength) : 0\n const totalBytes = Number.isSafeInteger(parsedContentLength)\n ? parsedContentLength\n : 0\n let transferredBytes = 0\n\n const stream = response.body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n transferredBytes += chunk.byteLength\n const percent =\n totalBytes > 0 ? Math.min(transferredBytes / totalBytes, 1) : 0\n onProgress({ percent, transferredBytes, totalBytes }, chunk)\n controller.enqueue(chunk)\n },\n })\n )\n\n return new Response(stream, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n })\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaO,SAAS,uBACd,YACc;AACd,SAAO;AAAA,IACL,MAAM;AAAA,IACN,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,YAAM,WAAW,MAAM,KAAK,GAAG;AAE/B,UAAI,CAAC,SAAS,MAAM;AAClB,eAAO;AAAA,MACT;AAEA,YAAM,gBAAgB,SAAS,QAAQ,IAAI,gBAAgB;AAC3D,YAAM,sBACJ,iBAAiB,QAAQ,KAAK,aAAa,IAAI,OAAO,aAAa,IAAI;AACzE,YAAM,aAAa,OAAO,cAAc,mBAAmB,IACvD,sBACA;AACJ,UAAI,mBAAmB;AAEvB,YAAM,SAAS,SAAS,KAAK;AAAA,QAC3B,IAAI,gBAAwC;AAAA,UAC1C,UAAU,OAAO,YAAY;AAC3B,gCAAoB,MAAM;AAC1B,kBAAM,UACJ,aAAa,IAAI,KAAK,IAAI,mBAAmB,YAAY,CAAC,IAAI;AAChE,uBAAW,EAAE,SAAS,kBAAkB,WAAW,GAAG,KAAK;AAC3D,uBAAW,QAAQ,KAAK;AAAA,UAC1B;AAAA,QACF,CAAC;AAAA,MACH;AAEA,aAAO,IAAI,SAAS,QAAQ;AAAA,QAC1B,QAAQ,SAAS;AAAA,QACjB,YAAY,SAAS;AAAA,QACrB,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
|
@@ -8,13 +8,14 @@ function downloadProgressPlugin(onProgress) {
|
|
|
8
8
|
return response;
|
|
9
9
|
}
|
|
10
10
|
const contentLength = response.headers.get("content-length");
|
|
11
|
-
const
|
|
11
|
+
const parsedContentLength = contentLength && /^\d+$/.test(contentLength) ? Number(contentLength) : 0;
|
|
12
|
+
const totalBytes = Number.isSafeInteger(parsedContentLength) ? parsedContentLength : 0;
|
|
12
13
|
let transferredBytes = 0;
|
|
13
14
|
const stream = response.body.pipeThrough(
|
|
14
15
|
new TransformStream({
|
|
15
16
|
transform(chunk, controller) {
|
|
16
17
|
transferredBytes += chunk.byteLength;
|
|
17
|
-
const percent = totalBytes > 0 ? transferredBytes / totalBytes : 0;
|
|
18
|
+
const percent = totalBytes > 0 ? Math.min(transferredBytes / totalBytes, 1) : 0;
|
|
18
19
|
onProgress({ percent, transferredBytes, totalBytes }, chunk);
|
|
19
20
|
controller.enqueue(chunk);
|
|
20
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/download-progress.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nexport type DownloadProgressEvent = {\n percent: number\n transferredBytes: number\n totalBytes: number\n}\n\nexport type DownloadProgressCallback = (\n progress: DownloadProgressEvent,\n chunk: Uint8Array\n) => void\n\nexport function downloadProgressPlugin(\n onProgress: DownloadProgressCallback\n): ClientPlugin {\n return {\n name: 'downloadProgress',\n wrapDispatch: (next) => async (ctx) => {\n const response = await next(ctx)\n\n if (!response.body) {\n return response\n }\n\n const contentLength = response.headers.get('content-length')\n const
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/download-progress.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nexport type DownloadProgressEvent = {\n percent: number\n transferredBytes: number\n totalBytes: number\n}\n\nexport type DownloadProgressCallback = (\n progress: DownloadProgressEvent,\n chunk: Uint8Array\n) => void\n\nexport function downloadProgressPlugin(\n onProgress: DownloadProgressCallback\n): ClientPlugin {\n return {\n name: 'downloadProgress',\n wrapDispatch: (next) => async (ctx) => {\n const response = await next(ctx)\n\n if (!response.body) {\n return response\n }\n\n const contentLength = response.headers.get('content-length')\n const parsedContentLength =\n contentLength && /^\\d+$/.test(contentLength) ? Number(contentLength) : 0\n const totalBytes = Number.isSafeInteger(parsedContentLength)\n ? parsedContentLength\n : 0\n let transferredBytes = 0\n\n const stream = response.body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n transferredBytes += chunk.byteLength\n const percent =\n totalBytes > 0 ? Math.min(transferredBytes / totalBytes, 1) : 0\n onProgress({ percent, transferredBytes, totalBytes }, chunk)\n controller.enqueue(chunk)\n },\n })\n )\n\n return new Response(stream, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n })\n },\n }\n}\n"],"mappings":";AAaO,SAAS,uBACd,YACc;AACd,SAAO;AAAA,IACL,MAAM;AAAA,IACN,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,YAAM,WAAW,MAAM,KAAK,GAAG;AAE/B,UAAI,CAAC,SAAS,MAAM;AAClB,eAAO;AAAA,MACT;AAEA,YAAM,gBAAgB,SAAS,QAAQ,IAAI,gBAAgB;AAC3D,YAAM,sBACJ,iBAAiB,QAAQ,KAAK,aAAa,IAAI,OAAO,aAAa,IAAI;AACzE,YAAM,aAAa,OAAO,cAAc,mBAAmB,IACvD,sBACA;AACJ,UAAI,mBAAmB;AAEvB,YAAM,SAAS,SAAS,KAAK;AAAA,QAC3B,IAAI,gBAAwC;AAAA,UAC1C,UAAU,OAAO,YAAY;AAC3B,gCAAoB,MAAM;AAC1B,kBAAM,UACJ,aAAa,IAAI,KAAK,IAAI,mBAAmB,YAAY,CAAC,IAAI;AAChE,uBAAW,EAAE,SAAS,kBAAkB,WAAW,GAAG,KAAK;AAC3D,uBAAW,QAAQ,KAAK;AAAA,UAC1B;AAAA,QACF,CAAC;AAAA,MACH;AAEA,aAAO,IAAI,SAAS,QAAQ;AAAA,QAC1B,QAAQ,SAAS;AAAA,QACjB,YAAY,SAAS;AAAA,QACrB,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
package/dist/plugins/hedge.cjs
CHANGED
|
@@ -45,8 +45,9 @@ function hedgePlugin(options) {
|
|
|
45
45
|
function launch(attemptIndex) {
|
|
46
46
|
const controller = new AbortController();
|
|
47
47
|
controllers.push(controller);
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const parentSignal = ctx.metadata.signals.combined ?? ctx.request.signal;
|
|
49
|
+
const signal = AbortSignal.any([parentSignal, controller.signal]);
|
|
50
|
+
const req = new Request(ctx.request.clone(), { signal });
|
|
50
51
|
attempts.push(next({ ...ctx, request: req }));
|
|
51
52
|
if (attemptIndex > 0) {
|
|
52
53
|
onHedge?.(ctx.request, attemptIndex);
|
|
@@ -61,6 +62,9 @@ function hedgePlugin(options) {
|
|
|
61
62
|
return new Promise((resolve, reject) => {
|
|
62
63
|
let settled = false;
|
|
63
64
|
let launched = 1;
|
|
65
|
+
let completed = 0;
|
|
66
|
+
let fallbackResponse;
|
|
67
|
+
let lastError;
|
|
64
68
|
const timers = [];
|
|
65
69
|
function settle(winnerIndex, value) {
|
|
66
70
|
settled = true;
|
|
@@ -68,37 +72,33 @@ function hedgePlugin(options) {
|
|
|
68
72
|
abortLosers(winnerIndex);
|
|
69
73
|
resolve(value);
|
|
70
74
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
function finishWithoutWinner() {
|
|
76
|
+
if (settled || completed < launched) return;
|
|
77
|
+
const allAttemptsLaunched = launched >= maxHedges + 1;
|
|
78
|
+
if (fallbackResponse && allAttemptsLaunched) {
|
|
79
|
+
settle(attempts.length - 1, fallbackResponse);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!fallbackResponse && lastError !== void 0) {
|
|
74
83
|
settled = true;
|
|
75
84
|
timers.forEach(clearTimeout);
|
|
76
|
-
let lastError = err;
|
|
77
|
-
for (const r of results) {
|
|
78
|
-
if (r.status === "rejected") lastError = r.reason;
|
|
79
|
-
}
|
|
80
85
|
reject(lastError);
|
|
81
|
-
}
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
function onAttemptSettled(index, result) {
|
|
84
89
|
if (settled) return;
|
|
90
|
+
completed++;
|
|
85
91
|
if (result.status === "fulfilled") {
|
|
86
92
|
const res = result.value;
|
|
87
|
-
|
|
88
|
-
if (res.ok || res.status < 500 && res.status !== 429 || isLastAttempt) {
|
|
93
|
+
if (res.ok || res.status < 500 && res.status !== 429) {
|
|
89
94
|
settle(index, res);
|
|
95
|
+
return;
|
|
90
96
|
}
|
|
97
|
+
fallbackResponse = res;
|
|
91
98
|
} else {
|
|
92
|
-
|
|
93
|
-
if (settled) return;
|
|
94
|
-
const pending = results.some(
|
|
95
|
-
(r) => r.status === "pending"
|
|
96
|
-
);
|
|
97
|
-
if (!pending) {
|
|
98
|
-
tryReject(result.reason);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
99
|
+
lastError = result.reason;
|
|
101
100
|
}
|
|
101
|
+
finishWithoutWinner();
|
|
102
102
|
}
|
|
103
103
|
function watchAttempt(index) {
|
|
104
104
|
attempts[index].then(
|
|
@@ -110,9 +110,9 @@ function hedgePlugin(options) {
|
|
|
110
110
|
for (let h = 1; h <= maxHedges; h++) {
|
|
111
111
|
const hedgeIndex = h;
|
|
112
112
|
const t = setTimeout(() => {
|
|
113
|
+
launched++;
|
|
113
114
|
launch(hedgeIndex);
|
|
114
115
|
watchAttempt(hedgeIndex);
|
|
115
|
-
launched++;
|
|
116
116
|
}, delayMs * hedgeIndex);
|
|
117
117
|
timers.push(t);
|
|
118
118
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/hedge.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'])\n\nexport type HedgePluginOptions = {\n delay: number | ((req: Request) => number)\n maxHedges?: number\n shouldHedge?: (req: Request) => boolean\n onHedge?: (req: Request, attempt: number) => void | Promise<void>\n order?: number\n}\n\nexport function hedgePlugin(options: HedgePluginOptions): ClientPlugin {\n const {\n delay,\n maxHedges = 1,\n shouldHedge = (req) => SAFE_METHODS.has(req.method),\n onHedge,\n order = 15,\n } = options\n\n return {\n name: 'hedge',\n order,\n wrapDispatch: (next) => async (ctx) => {\n if (!shouldHedge(ctx.request)) {\n return next(ctx)\n }\n\n const delayMs = typeof delay === 'function' ? delay(ctx.request) : delay\n\n const controllers: AbortController[] = []\n const attempts: Promise<Response>[] = []\n\n function launch(attemptIndex: number): void {\n const controller = new AbortController()\n controllers.push(controller)\n\n const signal = AbortSignal.any([
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/hedge.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'])\n\nexport type HedgePluginOptions = {\n delay: number | ((req: Request) => number)\n maxHedges?: number\n shouldHedge?: (req: Request) => boolean\n onHedge?: (req: Request, attempt: number) => void | Promise<void>\n order?: number\n}\n\nexport function hedgePlugin(options: HedgePluginOptions): ClientPlugin {\n const {\n delay,\n maxHedges = 1,\n shouldHedge = (req) => SAFE_METHODS.has(req.method),\n onHedge,\n order = 15,\n } = options\n\n return {\n name: 'hedge',\n order,\n wrapDispatch: (next) => async (ctx) => {\n if (!shouldHedge(ctx.request)) {\n return next(ctx)\n }\n\n const delayMs = typeof delay === 'function' ? delay(ctx.request) : delay\n\n const controllers: AbortController[] = []\n const attempts: Promise<Response>[] = []\n\n function launch(attemptIndex: number): void {\n const controller = new AbortController()\n controllers.push(controller)\n\n const parentSignal = ctx.metadata.signals.combined ?? ctx.request.signal\n const signal = AbortSignal.any([parentSignal, controller.signal])\n\n const req = new Request(ctx.request.clone(), { signal })\n attempts.push(next({ ...ctx, request: req }))\n\n if (attemptIndex > 0) {\n onHedge?.(ctx.request, attemptIndex)\n }\n }\n\n function abortLosers(winnerIndex: number): void {\n controllers.forEach((c, i) => {\n if (i !== winnerIndex) c.abort()\n })\n }\n\n // Launch initial attempt\n launch(0)\n\n return new Promise<Response>((resolve, reject) => {\n let settled = false\n let launched = 1\n let completed = 0\n let fallbackResponse: Response | undefined\n let lastError: unknown\n const timers: ReturnType<typeof setTimeout>[] = []\n\n function settle(winnerIndex: number, value: Response): void {\n settled = true\n timers.forEach(clearTimeout)\n abortLosers(winnerIndex)\n resolve(value)\n }\n\n function finishWithoutWinner(): void {\n if (settled || completed < launched) return\n\n const allAttemptsLaunched = launched >= maxHedges + 1\n if (fallbackResponse && allAttemptsLaunched) {\n settle(attempts.length - 1, fallbackResponse)\n return\n }\n\n // Preserve the existing fail-fast behavior for transport errors. A\n // retryable HTTP response, however, waits for all scheduled hedges.\n if (!fallbackResponse && lastError !== undefined) {\n settled = true\n timers.forEach(clearTimeout)\n reject(lastError)\n }\n }\n\n function onAttemptSettled(\n index: number,\n result: PromiseSettledResult<Response>\n ): void {\n if (settled) return\n\n completed++\n\n if (result.status === 'fulfilled') {\n const res = result.value\n // A retryable response is only a fallback. Its attempt index says\n // when it was launched, not whether a better attempt is pending.\n if (res.ok || (res.status < 500 && res.status !== 429)) {\n settle(index, res)\n return\n }\n fallbackResponse = res\n } else {\n lastError = result.reason\n }\n\n finishWithoutWinner()\n }\n\n function watchAttempt(index: number): void {\n attempts[index].then(\n (res) =>\n onAttemptSettled(index, { status: 'fulfilled', value: res }),\n (err) =>\n onAttemptSettled(index, { status: 'rejected', reason: err })\n )\n }\n\n watchAttempt(0)\n\n for (let h = 1; h <= maxHedges; h++) {\n const hedgeIndex = h\n const t = setTimeout(() => {\n launched++\n launch(hedgeIndex)\n watchAttempt(hedgeIndex)\n }, delayMs * hedgeIndex)\n timers.push(t)\n }\n })\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/plugins/hedge.js
CHANGED
|
@@ -21,8 +21,9 @@ function hedgePlugin(options) {
|
|
|
21
21
|
function launch(attemptIndex) {
|
|
22
22
|
const controller = new AbortController();
|
|
23
23
|
controllers.push(controller);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
24
|
+
const parentSignal = ctx.metadata.signals.combined ?? ctx.request.signal;
|
|
25
|
+
const signal = AbortSignal.any([parentSignal, controller.signal]);
|
|
26
|
+
const req = new Request(ctx.request.clone(), { signal });
|
|
26
27
|
attempts.push(next({ ...ctx, request: req }));
|
|
27
28
|
if (attemptIndex > 0) {
|
|
28
29
|
onHedge?.(ctx.request, attemptIndex);
|
|
@@ -37,6 +38,9 @@ function hedgePlugin(options) {
|
|
|
37
38
|
return new Promise((resolve, reject) => {
|
|
38
39
|
let settled = false;
|
|
39
40
|
let launched = 1;
|
|
41
|
+
let completed = 0;
|
|
42
|
+
let fallbackResponse;
|
|
43
|
+
let lastError;
|
|
40
44
|
const timers = [];
|
|
41
45
|
function settle(winnerIndex, value) {
|
|
42
46
|
settled = true;
|
|
@@ -44,37 +48,33 @@ function hedgePlugin(options) {
|
|
|
44
48
|
abortLosers(winnerIndex);
|
|
45
49
|
resolve(value);
|
|
46
50
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
function finishWithoutWinner() {
|
|
52
|
+
if (settled || completed < launched) return;
|
|
53
|
+
const allAttemptsLaunched = launched >= maxHedges + 1;
|
|
54
|
+
if (fallbackResponse && allAttemptsLaunched) {
|
|
55
|
+
settle(attempts.length - 1, fallbackResponse);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!fallbackResponse && lastError !== void 0) {
|
|
50
59
|
settled = true;
|
|
51
60
|
timers.forEach(clearTimeout);
|
|
52
|
-
let lastError = err;
|
|
53
|
-
for (const r of results) {
|
|
54
|
-
if (r.status === "rejected") lastError = r.reason;
|
|
55
|
-
}
|
|
56
61
|
reject(lastError);
|
|
57
|
-
}
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
function onAttemptSettled(index, result) {
|
|
60
65
|
if (settled) return;
|
|
66
|
+
completed++;
|
|
61
67
|
if (result.status === "fulfilled") {
|
|
62
68
|
const res = result.value;
|
|
63
|
-
|
|
64
|
-
if (res.ok || res.status < 500 && res.status !== 429 || isLastAttempt) {
|
|
69
|
+
if (res.ok || res.status < 500 && res.status !== 429) {
|
|
65
70
|
settle(index, res);
|
|
71
|
+
return;
|
|
66
72
|
}
|
|
73
|
+
fallbackResponse = res;
|
|
67
74
|
} else {
|
|
68
|
-
|
|
69
|
-
if (settled) return;
|
|
70
|
-
const pending = results.some(
|
|
71
|
-
(r) => r.status === "pending"
|
|
72
|
-
);
|
|
73
|
-
if (!pending) {
|
|
74
|
-
tryReject(result.reason);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
75
|
+
lastError = result.reason;
|
|
77
76
|
}
|
|
77
|
+
finishWithoutWinner();
|
|
78
78
|
}
|
|
79
79
|
function watchAttempt(index) {
|
|
80
80
|
attempts[index].then(
|
|
@@ -86,9 +86,9 @@ function hedgePlugin(options) {
|
|
|
86
86
|
for (let h = 1; h <= maxHedges; h++) {
|
|
87
87
|
const hedgeIndex = h;
|
|
88
88
|
const t = setTimeout(() => {
|
|
89
|
+
launched++;
|
|
89
90
|
launch(hedgeIndex);
|
|
90
91
|
watchAttempt(hedgeIndex);
|
|
91
|
-
launched++;
|
|
92
92
|
}, delayMs * hedgeIndex);
|
|
93
93
|
timers.push(t);
|
|
94
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/hedge.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'])\n\nexport type HedgePluginOptions = {\n delay: number | ((req: Request) => number)\n maxHedges?: number\n shouldHedge?: (req: Request) => boolean\n onHedge?: (req: Request, attempt: number) => void | Promise<void>\n order?: number\n}\n\nexport function hedgePlugin(options: HedgePluginOptions): ClientPlugin {\n const {\n delay,\n maxHedges = 1,\n shouldHedge = (req) => SAFE_METHODS.has(req.method),\n onHedge,\n order = 15,\n } = options\n\n return {\n name: 'hedge',\n order,\n wrapDispatch: (next) => async (ctx) => {\n if (!shouldHedge(ctx.request)) {\n return next(ctx)\n }\n\n const delayMs = typeof delay === 'function' ? delay(ctx.request) : delay\n\n const controllers: AbortController[] = []\n const attempts: Promise<Response>[] = []\n\n function launch(attemptIndex: number): void {\n const controller = new AbortController()\n controllers.push(controller)\n\n const signal = AbortSignal.any([
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/hedge.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'])\n\nexport type HedgePluginOptions = {\n delay: number | ((req: Request) => number)\n maxHedges?: number\n shouldHedge?: (req: Request) => boolean\n onHedge?: (req: Request, attempt: number) => void | Promise<void>\n order?: number\n}\n\nexport function hedgePlugin(options: HedgePluginOptions): ClientPlugin {\n const {\n delay,\n maxHedges = 1,\n shouldHedge = (req) => SAFE_METHODS.has(req.method),\n onHedge,\n order = 15,\n } = options\n\n return {\n name: 'hedge',\n order,\n wrapDispatch: (next) => async (ctx) => {\n if (!shouldHedge(ctx.request)) {\n return next(ctx)\n }\n\n const delayMs = typeof delay === 'function' ? delay(ctx.request) : delay\n\n const controllers: AbortController[] = []\n const attempts: Promise<Response>[] = []\n\n function launch(attemptIndex: number): void {\n const controller = new AbortController()\n controllers.push(controller)\n\n const parentSignal = ctx.metadata.signals.combined ?? ctx.request.signal\n const signal = AbortSignal.any([parentSignal, controller.signal])\n\n const req = new Request(ctx.request.clone(), { signal })\n attempts.push(next({ ...ctx, request: req }))\n\n if (attemptIndex > 0) {\n onHedge?.(ctx.request, attemptIndex)\n }\n }\n\n function abortLosers(winnerIndex: number): void {\n controllers.forEach((c, i) => {\n if (i !== winnerIndex) c.abort()\n })\n }\n\n // Launch initial attempt\n launch(0)\n\n return new Promise<Response>((resolve, reject) => {\n let settled = false\n let launched = 1\n let completed = 0\n let fallbackResponse: Response | undefined\n let lastError: unknown\n const timers: ReturnType<typeof setTimeout>[] = []\n\n function settle(winnerIndex: number, value: Response): void {\n settled = true\n timers.forEach(clearTimeout)\n abortLosers(winnerIndex)\n resolve(value)\n }\n\n function finishWithoutWinner(): void {\n if (settled || completed < launched) return\n\n const allAttemptsLaunched = launched >= maxHedges + 1\n if (fallbackResponse && allAttemptsLaunched) {\n settle(attempts.length - 1, fallbackResponse)\n return\n }\n\n // Preserve the existing fail-fast behavior for transport errors. A\n // retryable HTTP response, however, waits for all scheduled hedges.\n if (!fallbackResponse && lastError !== undefined) {\n settled = true\n timers.forEach(clearTimeout)\n reject(lastError)\n }\n }\n\n function onAttemptSettled(\n index: number,\n result: PromiseSettledResult<Response>\n ): void {\n if (settled) return\n\n completed++\n\n if (result.status === 'fulfilled') {\n const res = result.value\n // A retryable response is only a fallback. Its attempt index says\n // when it was launched, not whether a better attempt is pending.\n if (res.ok || (res.status < 500 && res.status !== 429)) {\n settle(index, res)\n return\n }\n fallbackResponse = res\n } else {\n lastError = result.reason\n }\n\n finishWithoutWinner()\n }\n\n function watchAttempt(index: number): void {\n attempts[index].then(\n (res) =>\n onAttemptSettled(index, { status: 'fulfilled', value: res }),\n (err) =>\n onAttemptSettled(index, { status: 'rejected', reason: err })\n )\n }\n\n watchAttempt(0)\n\n for (let h = 1; h <= maxHedges; h++) {\n const hedgeIndex = h\n const t = setTimeout(() => {\n launched++\n launch(hedgeIndex)\n watchAttempt(hedgeIndex)\n }, delayMs * hedgeIndex)\n timers.push(t)\n }\n })\n },\n }\n}\n"],"mappings":";AAEA,IAAM,eAAe,oBAAI,IAAI,CAAC,OAAO,QAAQ,WAAW,OAAO,QAAQ,CAAC;AAUjE,SAAS,YAAY,SAA2C;AACrE,QAAM;AAAA,IACJ;AAAA,IACA,YAAY;AAAA,IACZ,cAAc,CAAC,QAAQ,aAAa,IAAI,IAAI,MAAM;AAAA,IAClD;AAAA,IACA,QAAQ;AAAA,EACV,IAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,UAAI,CAAC,YAAY,IAAI,OAAO,GAAG;AAC7B,eAAO,KAAK,GAAG;AAAA,MACjB;AAEA,YAAM,UAAU,OAAO,UAAU,aAAa,MAAM,IAAI,OAAO,IAAI;AAEnE,YAAM,cAAiC,CAAC;AACxC,YAAM,WAAgC,CAAC;AAEvC,eAAS,OAAO,cAA4B;AAC1C,cAAM,aAAa,IAAI,gBAAgB;AACvC,oBAAY,KAAK,UAAU;AAE3B,cAAM,eAAe,IAAI,SAAS,QAAQ,YAAY,IAAI,QAAQ;AAClE,cAAM,SAAS,YAAY,IAAI,CAAC,cAAc,WAAW,MAAM,CAAC;AAEhE,cAAM,MAAM,IAAI,QAAQ,IAAI,QAAQ,MAAM,GAAG,EAAE,OAAO,CAAC;AACvD,iBAAS,KAAK,KAAK,EAAE,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC;AAE5C,YAAI,eAAe,GAAG;AACpB,oBAAU,IAAI,SAAS,YAAY;AAAA,QACrC;AAAA,MACF;AAEA,eAAS,YAAY,aAA2B;AAC9C,oBAAY,QAAQ,CAAC,GAAG,MAAM;AAC5B,cAAI,MAAM,YAAa,GAAE,MAAM;AAAA,QACjC,CAAC;AAAA,MACH;AAGA,aAAO,CAAC;AAER,aAAO,IAAI,QAAkB,CAAC,SAAS,WAAW;AAChD,YAAI,UAAU;AACd,YAAI,WAAW;AACf,YAAI,YAAY;AAChB,YAAI;AACJ,YAAI;AACJ,cAAM,SAA0C,CAAC;AAEjD,iBAAS,OAAO,aAAqB,OAAuB;AAC1D,oBAAU;AACV,iBAAO,QAAQ,YAAY;AAC3B,sBAAY,WAAW;AACvB,kBAAQ,KAAK;AAAA,QACf;AAEA,iBAAS,sBAA4B;AACnC,cAAI,WAAW,YAAY,SAAU;AAErC,gBAAM,sBAAsB,YAAY,YAAY;AACpD,cAAI,oBAAoB,qBAAqB;AAC3C,mBAAO,SAAS,SAAS,GAAG,gBAAgB;AAC5C;AAAA,UACF;AAIA,cAAI,CAAC,oBAAoB,cAAc,QAAW;AAChD,sBAAU;AACV,mBAAO,QAAQ,YAAY;AAC3B,mBAAO,SAAS;AAAA,UAClB;AAAA,QACF;AAEA,iBAAS,iBACP,OACA,QACM;AACN,cAAI,QAAS;AAEb;AAEA,cAAI,OAAO,WAAW,aAAa;AACjC,kBAAM,MAAM,OAAO;AAGnB,gBAAI,IAAI,MAAO,IAAI,SAAS,OAAO,IAAI,WAAW,KAAM;AACtD,qBAAO,OAAO,GAAG;AACjB;AAAA,YACF;AACA,+BAAmB;AAAA,UACrB,OAAO;AACL,wBAAY,OAAO;AAAA,UACrB;AAEA,8BAAoB;AAAA,QACtB;AAEA,iBAAS,aAAa,OAAqB;AACzC,mBAAS,KAAK,EAAE;AAAA,YACd,CAAC,QACC,iBAAiB,OAAO,EAAE,QAAQ,aAAa,OAAO,IAAI,CAAC;AAAA,YAC7D,CAAC,QACC,iBAAiB,OAAO,EAAE,QAAQ,YAAY,QAAQ,IAAI,CAAC;AAAA,UAC/D;AAAA,QACF;AAEA,qBAAa,CAAC;AAEd,iBAAS,IAAI,GAAG,KAAK,WAAW,KAAK;AACnC,gBAAM,aAAa;AACnB,gBAAM,IAAI,WAAW,MAAM;AACzB;AACA,mBAAO,UAAU;AACjB,yBAAa,UAAU;AAAA,UACzB,GAAG,UAAU,UAAU;AACvB,iBAAO,KAAK,CAAC;AAAA,QACf;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fetchkit/ffetch",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "Fetch wrapper with configurable timeouts, retries, and TypeScript-first DX",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/parser": "^8.39.0",
|
|
96
96
|
"@vitest/coverage-v8": "^4.1.8",
|
|
97
97
|
"eslint": "^10.4.1",
|
|
98
|
-
"
|
|
98
|
+
"fast-check": "^4.9.0",
|
|
99
99
|
"husky": "^9.1.7",
|
|
100
100
|
"jsr": "^0.14.3",
|
|
101
101
|
"lint-staged": "^17.0.5",
|