react-native-fetch-client 0.0.0-stage → 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +22 -0
- package/README.md +806 -2
- package/dist/index.cjs +2888 -0
- package/dist/index.d.cts +1744 -0
- package/dist/index.d.ts +1744 -0
- package/dist/index.mjs +2849 -0
- package/package.json +76 -4
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2849 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
function buildOptions(partial) {
|
|
3
|
+
const opts = {};
|
|
4
|
+
if (partial.config !== void 0) opts.config = partial.config;
|
|
5
|
+
if (partial.cause !== void 0) opts.cause = partial.cause;
|
|
6
|
+
return opts;
|
|
7
|
+
}
|
|
8
|
+
var FetchNativeError = class extends Error {
|
|
9
|
+
/** Discriminant code identifying the specific error type. */
|
|
10
|
+
code;
|
|
11
|
+
/**
|
|
12
|
+
* The resolved request configuration at the time of the error.
|
|
13
|
+
* Authorization header values are redacted to '[REDACTED]'.
|
|
14
|
+
*/
|
|
15
|
+
config;
|
|
16
|
+
constructor(message, code, options) {
|
|
17
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
18
|
+
this.name = "FetchNativeError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
if (options?.config !== void 0) {
|
|
21
|
+
this.config = options.config;
|
|
22
|
+
}
|
|
23
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var HttpError = class extends FetchNativeError {
|
|
27
|
+
/** HTTP status code (e.g. 404, 500). */
|
|
28
|
+
status;
|
|
29
|
+
/** HTTP status text (e.g. 'Not Found', 'Internal Server Error'). */
|
|
30
|
+
statusText;
|
|
31
|
+
/**
|
|
32
|
+
* Parsed response body, if available and parseable.
|
|
33
|
+
* May be a string, object, or null depending on the response content.
|
|
34
|
+
*/
|
|
35
|
+
data;
|
|
36
|
+
/** Response headers from the server. Keys are normalized to lowercase. */
|
|
37
|
+
headers;
|
|
38
|
+
/**
|
|
39
|
+
* The original Fetch Response object.
|
|
40
|
+
* Do not log this directly — it may contain sensitive header values.
|
|
41
|
+
*/
|
|
42
|
+
response;
|
|
43
|
+
constructor(message, options) {
|
|
44
|
+
super(message, "HTTP_ERROR", buildOptions({ config: options.config }));
|
|
45
|
+
this.name = "HttpError";
|
|
46
|
+
this.status = options.status;
|
|
47
|
+
this.statusText = options.statusText;
|
|
48
|
+
this.data = options.data;
|
|
49
|
+
this.headers = options.headers;
|
|
50
|
+
this.response = options.response;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var NetworkError = class extends FetchNativeError {
|
|
54
|
+
constructor(message, options) {
|
|
55
|
+
super(message, "NETWORK_ERROR", options !== void 0 ? buildOptions(options) : void 0);
|
|
56
|
+
this.name = "NetworkError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var TimeoutError = class extends FetchNativeError {
|
|
60
|
+
/** The timeout that was exceeded, in milliseconds. */
|
|
61
|
+
timeoutMs;
|
|
62
|
+
constructor(timeoutMs, options) {
|
|
63
|
+
super(
|
|
64
|
+
`Request timed out after ${timeoutMs}ms`,
|
|
65
|
+
"TIMEOUT",
|
|
66
|
+
options !== void 0 ? buildOptions(options) : void 0
|
|
67
|
+
);
|
|
68
|
+
this.name = "TimeoutError";
|
|
69
|
+
this.timeoutMs = timeoutMs;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var CancelError = class extends FetchNativeError {
|
|
73
|
+
constructor(options) {
|
|
74
|
+
super(
|
|
75
|
+
"Request was cancelled",
|
|
76
|
+
"CANCELLED",
|
|
77
|
+
options !== void 0 ? buildOptions(options) : void 0
|
|
78
|
+
);
|
|
79
|
+
this.name = "CancelError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ParseError = class extends FetchNativeError {
|
|
83
|
+
/** HTTP status code of the response whose body could not be parsed. */
|
|
84
|
+
status;
|
|
85
|
+
/** The raw response body text that failed to parse, if retrievable. */
|
|
86
|
+
rawBody;
|
|
87
|
+
constructor(message, options) {
|
|
88
|
+
super(message, "PARSE_ERROR", buildOptions({ config: options.config, cause: options.cause }));
|
|
89
|
+
this.name = "ParseError";
|
|
90
|
+
this.status = options.status;
|
|
91
|
+
this.rawBody = options.rawBody;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var ConfigError = class extends FetchNativeError {
|
|
95
|
+
constructor(message, options) {
|
|
96
|
+
super(
|
|
97
|
+
message,
|
|
98
|
+
"CONFIG_ERROR",
|
|
99
|
+
options?.cause !== void 0 ? { cause: options.cause } : void 0
|
|
100
|
+
);
|
|
101
|
+
this.name = "ConfigError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var RetryExhaustedError = class extends FetchNativeError {
|
|
105
|
+
/** The total number of attempts made (initial + retries). */
|
|
106
|
+
attempts;
|
|
107
|
+
/** The error from the last failed attempt. */
|
|
108
|
+
lastError;
|
|
109
|
+
constructor(attempts, lastError, options) {
|
|
110
|
+
const cause = lastError instanceof Error ? lastError : void 0;
|
|
111
|
+
super(
|
|
112
|
+
`Request failed after ${attempts} attempt${attempts === 1 ? "" : "s"}`,
|
|
113
|
+
"RETRY_EXHAUSTED",
|
|
114
|
+
buildOptions({ config: options?.config, cause })
|
|
115
|
+
);
|
|
116
|
+
this.name = "RetryExhaustedError";
|
|
117
|
+
this.attempts = attempts;
|
|
118
|
+
this.lastError = lastError;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var ValidationError = class extends FetchNativeError {
|
|
122
|
+
/** The raw unvalidated data returned by the server. */
|
|
123
|
+
data;
|
|
124
|
+
/** Detailed validation issue objects or messages if provided by the validator. */
|
|
125
|
+
issues;
|
|
126
|
+
constructor(message, options) {
|
|
127
|
+
super(message, "VALIDATION_ERROR", options !== void 0 ? buildOptions(options) : void 0);
|
|
128
|
+
this.name = "ValidationError";
|
|
129
|
+
this.data = options?.data;
|
|
130
|
+
if (options?.issues !== void 0) {
|
|
131
|
+
this.issues = options.issues;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var CircuitBreakerError = class extends FetchNativeError {
|
|
136
|
+
/** Current state of the circuit breaker at the moment of the rejection ('OPEN' | 'HALF_OPEN'). */
|
|
137
|
+
state;
|
|
138
|
+
/** Configured cooldown period before canary trial in milliseconds. */
|
|
139
|
+
resetTimeoutMs;
|
|
140
|
+
constructor(message, options) {
|
|
141
|
+
super(message, "CIRCUIT_OPEN", buildOptions(options));
|
|
142
|
+
this.name = "CircuitBreakerError";
|
|
143
|
+
this.state = options.state;
|
|
144
|
+
this.resetTimeoutMs = options.resetTimeoutMs;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function isFetchNativeError(err) {
|
|
148
|
+
return err instanceof FetchNativeError;
|
|
149
|
+
}
|
|
150
|
+
function isHttpError(err) {
|
|
151
|
+
return err instanceof HttpError;
|
|
152
|
+
}
|
|
153
|
+
function isNetworkError(err) {
|
|
154
|
+
return err instanceof NetworkError;
|
|
155
|
+
}
|
|
156
|
+
function isTimeoutError(err) {
|
|
157
|
+
return err instanceof TimeoutError;
|
|
158
|
+
}
|
|
159
|
+
function isCancelError(err) {
|
|
160
|
+
return err instanceof CancelError;
|
|
161
|
+
}
|
|
162
|
+
function isRetryExhaustedError(err) {
|
|
163
|
+
return err instanceof RetryExhaustedError;
|
|
164
|
+
}
|
|
165
|
+
function isValidationError(err) {
|
|
166
|
+
return err instanceof ValidationError;
|
|
167
|
+
}
|
|
168
|
+
function isCircuitBreakerError(err) {
|
|
169
|
+
return err instanceof CircuitBreakerError;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/url.ts
|
|
173
|
+
function interpolatePath(url, pathParams) {
|
|
174
|
+
if (!pathParams || Object.keys(pathParams).length === 0) {
|
|
175
|
+
return url;
|
|
176
|
+
}
|
|
177
|
+
let result = url;
|
|
178
|
+
for (const [key, val] of Object.entries(pathParams)) {
|
|
179
|
+
if (val !== void 0 && val !== null) {
|
|
180
|
+
const encoded = encodeURIComponent(String(val));
|
|
181
|
+
const colonPattern = new RegExp(`:${key}(?=[/?#]|$)`, "g");
|
|
182
|
+
result = result.replace(colonPattern, encoded);
|
|
183
|
+
const bracePattern = new RegExp(`\\{${key}\\}`, "g");
|
|
184
|
+
result = result.replace(bracePattern, encoded);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
function buildUrl(url = "", baseURL, params, pathParams) {
|
|
190
|
+
let resolvedUrl = interpolatePath(url, pathParams);
|
|
191
|
+
if (baseURL && !/^([a-z][a-z0-9+\-.]*:)?\/\//i.test(resolvedUrl)) {
|
|
192
|
+
const cleanBase = baseURL.replace(/\/+$/, "");
|
|
193
|
+
const cleanPath = resolvedUrl.replace(/^\/+/, "");
|
|
194
|
+
resolvedUrl = cleanPath ? `${cleanBase}/${cleanPath}` : cleanBase;
|
|
195
|
+
}
|
|
196
|
+
if (params) {
|
|
197
|
+
const searchParams = new URLSearchParams();
|
|
198
|
+
const [base, search] = resolvedUrl.split("?");
|
|
199
|
+
if (search) {
|
|
200
|
+
const existing = new URLSearchParams(search);
|
|
201
|
+
existing.forEach((val, key) => searchParams.append(key, val));
|
|
202
|
+
}
|
|
203
|
+
for (const [key, value] of Object.entries(params)) {
|
|
204
|
+
if (value !== null && value !== void 0) {
|
|
205
|
+
searchParams.append(key, String(value));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const qs = searchParams.toString();
|
|
209
|
+
return qs ? `${base}?${qs}` : base || "";
|
|
210
|
+
}
|
|
211
|
+
return resolvedUrl;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/headers.ts
|
|
215
|
+
function mergeHeaders(...headersList) {
|
|
216
|
+
const merged = {};
|
|
217
|
+
for (const headers of headersList) {
|
|
218
|
+
if (!headers) continue;
|
|
219
|
+
const h = new Headers(headers);
|
|
220
|
+
h.forEach((value, key) => {
|
|
221
|
+
merged[key.toLowerCase()] = value;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return merged;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/serialize.ts
|
|
228
|
+
function serializeBody(body, headers, formUrlEncoded) {
|
|
229
|
+
if (body === null || body === void 0) {
|
|
230
|
+
return { body: null };
|
|
231
|
+
}
|
|
232
|
+
if (typeof body === "string") {
|
|
233
|
+
return { body, contentType: "text/plain" };
|
|
234
|
+
}
|
|
235
|
+
if (typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
|
|
236
|
+
return { body };
|
|
237
|
+
}
|
|
238
|
+
const requestedContentType = headers ? headers["content-type"] ?? headers["Content-Type"] : void 0;
|
|
239
|
+
const isFormUrlEncoded = formUrlEncoded === true || typeof requestedContentType === "string" && requestedContentType.toLowerCase().includes("application/x-www-form-urlencoded");
|
|
240
|
+
if (isFormUrlEncoded && typeof body === "object") {
|
|
241
|
+
const params = new URLSearchParams();
|
|
242
|
+
for (const [key, value] of Object.entries(body)) {
|
|
243
|
+
if (value !== void 0 && value !== null) {
|
|
244
|
+
let strVal;
|
|
245
|
+
if (typeof value === "string") {
|
|
246
|
+
strVal = value;
|
|
247
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
248
|
+
strVal = value.toString();
|
|
249
|
+
} else {
|
|
250
|
+
strVal = JSON.stringify(value);
|
|
251
|
+
}
|
|
252
|
+
params.append(key, strVal);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
body: params.toString(),
|
|
257
|
+
contentType: "application/x-www-form-urlencoded;charset=UTF-8"
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { body: JSON.stringify(body), contentType: "application/json" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/parse.ts
|
|
264
|
+
async function readBodyWithProgress(response, onProgress) {
|
|
265
|
+
const body = response.body;
|
|
266
|
+
if (!body || typeof body.getReader !== "function") {
|
|
267
|
+
const ab = await response.arrayBuffer();
|
|
268
|
+
const u8 = new Uint8Array(ab);
|
|
269
|
+
if (onProgress) {
|
|
270
|
+
onProgress({
|
|
271
|
+
loaded: u8.length,
|
|
272
|
+
total: u8.length,
|
|
273
|
+
progress: 1,
|
|
274
|
+
bytes: u8.length
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return u8;
|
|
278
|
+
}
|
|
279
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
280
|
+
const total = contentLengthHeader ? parseInt(contentLengthHeader, 10) : void 0;
|
|
281
|
+
const isTotalValid = total !== void 0 && !isNaN(total) && total >= 0;
|
|
282
|
+
const reader = body.getReader();
|
|
283
|
+
const chunks = [];
|
|
284
|
+
let loaded = 0;
|
|
285
|
+
while (true) {
|
|
286
|
+
const { done, value } = await reader.read();
|
|
287
|
+
if (done) break;
|
|
288
|
+
if (value) {
|
|
289
|
+
chunks.push(value);
|
|
290
|
+
loaded += value.length;
|
|
291
|
+
if (onProgress) {
|
|
292
|
+
const event = {
|
|
293
|
+
loaded,
|
|
294
|
+
bytes: value.length
|
|
295
|
+
};
|
|
296
|
+
if (isTotalValid) {
|
|
297
|
+
event.total = total;
|
|
298
|
+
event.progress = loaded / total;
|
|
299
|
+
}
|
|
300
|
+
onProgress(event);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const combined = new Uint8Array(loaded);
|
|
305
|
+
let offset = 0;
|
|
306
|
+
for (const chunk of chunks) {
|
|
307
|
+
combined.set(chunk, offset);
|
|
308
|
+
offset += chunk.length;
|
|
309
|
+
}
|
|
310
|
+
return combined;
|
|
311
|
+
}
|
|
312
|
+
async function parseResponse(response, responseType, config, onDownloadProgress) {
|
|
313
|
+
if (responseType === "none" || response.status === 204) {
|
|
314
|
+
await response.text().catch(() => {
|
|
315
|
+
});
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (responseType === "stream") {
|
|
319
|
+
return response.body;
|
|
320
|
+
}
|
|
321
|
+
let bodyBytes;
|
|
322
|
+
if (onDownloadProgress) {
|
|
323
|
+
bodyBytes = await readBodyWithProgress(response, onDownloadProgress);
|
|
324
|
+
}
|
|
325
|
+
if (responseType === "json") {
|
|
326
|
+
let text;
|
|
327
|
+
if (bodyBytes) {
|
|
328
|
+
text = new TextDecoder().decode(bodyBytes);
|
|
329
|
+
} else {
|
|
330
|
+
text = await response.text();
|
|
331
|
+
}
|
|
332
|
+
if (!text) return null;
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(text);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
throw new ParseError("Failed to parse JSON response", {
|
|
337
|
+
status: response.status,
|
|
338
|
+
rawBody: text,
|
|
339
|
+
config,
|
|
340
|
+
cause: err
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (responseType === "text") {
|
|
345
|
+
if (bodyBytes) return new TextDecoder().decode(bodyBytes);
|
|
346
|
+
return response.text();
|
|
347
|
+
}
|
|
348
|
+
if (responseType === "blob") {
|
|
349
|
+
if (bodyBytes) {
|
|
350
|
+
const mime = response.headers.get("content-type") ?? "";
|
|
351
|
+
return new Blob([bodyBytes.buffer], { type: mime });
|
|
352
|
+
}
|
|
353
|
+
return response.blob();
|
|
354
|
+
}
|
|
355
|
+
if (responseType === "arrayBuffer") {
|
|
356
|
+
if (bodyBytes) return bodyBytes.buffer;
|
|
357
|
+
return response.arrayBuffer();
|
|
358
|
+
}
|
|
359
|
+
if (bodyBytes) return new TextDecoder().decode(bodyBytes);
|
|
360
|
+
return response.text();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/retry.ts
|
|
364
|
+
var DEFAULT_RETRY_POLICY = {
|
|
365
|
+
attempts: 0,
|
|
366
|
+
backoff: "exponential",
|
|
367
|
+
delay: 1e3,
|
|
368
|
+
maxDelay: 3e4,
|
|
369
|
+
jitter: true,
|
|
370
|
+
retryOn: [429, 500, 502, 503, 504],
|
|
371
|
+
safeMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
|
|
372
|
+
respectRetryAfter: true
|
|
373
|
+
};
|
|
374
|
+
function resolveRetryPolicy(clientRetry, requestRetry) {
|
|
375
|
+
if (requestRetry === false) {
|
|
376
|
+
return { ...DEFAULT_RETRY_POLICY, attempts: 0 };
|
|
377
|
+
}
|
|
378
|
+
const base = clientRetry === false ? { ...DEFAULT_RETRY_POLICY, attempts: 0 } : { ...DEFAULT_RETRY_POLICY, ...clientRetry };
|
|
379
|
+
const merged = { ...base, ...requestRetry };
|
|
380
|
+
const resolved = {
|
|
381
|
+
attempts: Math.max(0, merged.attempts ?? 0),
|
|
382
|
+
backoff: merged.backoff ?? "exponential",
|
|
383
|
+
delay: Math.max(0, merged.delay ?? 1e3),
|
|
384
|
+
maxDelay: Math.max(0, merged.maxDelay ?? 3e4),
|
|
385
|
+
jitter: merged.jitter ?? true,
|
|
386
|
+
retryOn: merged.retryOn ?? [429, 500, 502, 503, 504],
|
|
387
|
+
safeMethods: merged.safeMethods ?? ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
|
|
388
|
+
respectRetryAfter: merged.respectRetryAfter ?? true
|
|
389
|
+
};
|
|
390
|
+
if (merged.retryCondition !== void 0) {
|
|
391
|
+
resolved.retryCondition = merged.retryCondition;
|
|
392
|
+
}
|
|
393
|
+
return resolved;
|
|
394
|
+
}
|
|
395
|
+
function parseRetryAfter(headerValue) {
|
|
396
|
+
if (!headerValue || typeof headerValue !== "string") {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const trimmed = headerValue.trim();
|
|
400
|
+
if (/^\d+$/.test(trimmed)) {
|
|
401
|
+
const seconds = parseInt(trimmed, 10);
|
|
402
|
+
if (!isNaN(seconds) && seconds >= 0) {
|
|
403
|
+
return seconds * 1e3;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const dateMs = Date.parse(trimmed);
|
|
408
|
+
if (!isNaN(dateMs)) {
|
|
409
|
+
const diff = dateMs - Date.now();
|
|
410
|
+
return Math.max(0, diff);
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
function calculateRetryDelay(attempt, policy, error, randomFn = Math.random) {
|
|
415
|
+
if (policy.respectRetryAfter && error instanceof HttpError) {
|
|
416
|
+
const retryAfterHeader = error.headers["retry-after"];
|
|
417
|
+
const parsed = parseRetryAfter(retryAfterHeader);
|
|
418
|
+
if (parsed !== null) {
|
|
419
|
+
return Math.min(parsed, policy.maxDelay);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const normalizedAttempt = Math.max(1, attempt);
|
|
423
|
+
let delay;
|
|
424
|
+
if (policy.backoff === "linear") {
|
|
425
|
+
delay = policy.delay * normalizedAttempt;
|
|
426
|
+
} else {
|
|
427
|
+
delay = policy.delay * Math.pow(2, normalizedAttempt - 1);
|
|
428
|
+
}
|
|
429
|
+
delay = Math.min(delay, policy.maxDelay);
|
|
430
|
+
if (policy.jitter) {
|
|
431
|
+
delay = Math.floor(randomFn() * delay);
|
|
432
|
+
}
|
|
433
|
+
return Math.max(0, delay);
|
|
434
|
+
}
|
|
435
|
+
function shouldRetry(error, attempt, config) {
|
|
436
|
+
if (attempt > config.retry.attempts) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (!config.retry.safeMethods.includes(config.method)) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
if (error instanceof CancelError) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (error instanceof ConfigError) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
if (error instanceof ParseError) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (error instanceof TimeoutError) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (typeof config.retry.retryCondition === "function") {
|
|
455
|
+
return config.retry.retryCondition(error, attempt);
|
|
456
|
+
}
|
|
457
|
+
if (typeof config.retry.retryOn === "function") {
|
|
458
|
+
return config.retry.retryOn(error, attempt);
|
|
459
|
+
}
|
|
460
|
+
if (error instanceof HttpError) {
|
|
461
|
+
if (Array.isArray(config.retry.retryOn)) {
|
|
462
|
+
return config.retry.retryOn.includes(error.status);
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (error instanceof NetworkError) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
function sleep(ms, signal) {
|
|
472
|
+
if (signal?.aborted) {
|
|
473
|
+
const reason = signal.reason;
|
|
474
|
+
if (reason instanceof Error) {
|
|
475
|
+
return Promise.reject(reason);
|
|
476
|
+
}
|
|
477
|
+
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
478
|
+
}
|
|
479
|
+
if (ms <= 0) {
|
|
480
|
+
return Promise.resolve();
|
|
481
|
+
}
|
|
482
|
+
return new Promise((resolve, reject) => {
|
|
483
|
+
function onAbort() {
|
|
484
|
+
clearTimeout(timerId);
|
|
485
|
+
if (signal) {
|
|
486
|
+
signal.removeEventListener("abort", onAbort);
|
|
487
|
+
}
|
|
488
|
+
const reason = signal?.reason;
|
|
489
|
+
if (reason instanceof Error) {
|
|
490
|
+
reject(reason);
|
|
491
|
+
} else {
|
|
492
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (signal) {
|
|
496
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
497
|
+
}
|
|
498
|
+
const timerId = setTimeout(() => {
|
|
499
|
+
if (signal) {
|
|
500
|
+
signal.removeEventListener("abort", onAbort);
|
|
501
|
+
}
|
|
502
|
+
resolve();
|
|
503
|
+
}, ms);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/dedup.ts
|
|
508
|
+
function generateDedupKey(method, url, headers = {}, credentials, body) {
|
|
509
|
+
const m = method.toUpperCase();
|
|
510
|
+
let normalizedUrl = url;
|
|
511
|
+
try {
|
|
512
|
+
const parsed = new URL(url, "http://localhost");
|
|
513
|
+
parsed.searchParams.sort();
|
|
514
|
+
normalizedUrl = parsed.pathname + parsed.search;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
const relevantKeys = ["accept", "content-type", "authorization", "accept-language"];
|
|
518
|
+
const headerPairs = [];
|
|
519
|
+
for (const k of Object.keys(headers).sort()) {
|
|
520
|
+
const lk = k.toLowerCase();
|
|
521
|
+
if (relevantKeys.includes(lk) && headers[k]) {
|
|
522
|
+
headerPairs.push(`${lk}=${headers[k]}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const creds = credentials || "same-origin";
|
|
526
|
+
let bodyStr = "";
|
|
527
|
+
if (body !== void 0 && body !== null) {
|
|
528
|
+
if (typeof body === "string") {
|
|
529
|
+
bodyStr = body;
|
|
530
|
+
} else {
|
|
531
|
+
try {
|
|
532
|
+
bodyStr = JSON.stringify(body) ?? "";
|
|
533
|
+
} catch {
|
|
534
|
+
bodyStr = "[unserializable-body]";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return `${m}|${normalizedUrl}|${creds}|${headerPairs.join("&")}|${bodyStr}`;
|
|
539
|
+
}
|
|
540
|
+
function isDedupEligible(method, requestDedup, clientDedup) {
|
|
541
|
+
if (requestDedup === false) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
let enabled = false;
|
|
545
|
+
if (requestDedup === true) {
|
|
546
|
+
enabled = true;
|
|
547
|
+
} else if (typeof requestDedup === "object" && requestDedup.enabled !== void 0) {
|
|
548
|
+
enabled = requestDedup.enabled;
|
|
549
|
+
} else if (clientDedup?.enabled) {
|
|
550
|
+
enabled = clientDedup.enabled;
|
|
551
|
+
}
|
|
552
|
+
if (!enabled) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
const allowedMethods = typeof requestDedup === "object" && requestDedup.methods || clientDedup?.methods || ["GET", "HEAD"];
|
|
556
|
+
return allowedMethods.includes(method);
|
|
557
|
+
}
|
|
558
|
+
var DedupManager = class {
|
|
559
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
560
|
+
/**
|
|
561
|
+
* Executes or joins an in-flight request for the given deduplication key.
|
|
562
|
+
* If a matching request is already in flight, the caller subscribes to it.
|
|
563
|
+
* If an individual caller cancels their AbortSignal, that caller immediately rejects,
|
|
564
|
+
* but the underlying request continues for other subscribers.
|
|
565
|
+
* The underlying request is only aborted if all subscribers have cancelled.
|
|
566
|
+
*/
|
|
567
|
+
execute(key, factory, callerSignal) {
|
|
568
|
+
if (callerSignal?.aborted) {
|
|
569
|
+
const reason = callerSignal.reason;
|
|
570
|
+
if (reason instanceof CancelError) {
|
|
571
|
+
return Promise.reject(reason);
|
|
572
|
+
}
|
|
573
|
+
return Promise.reject(
|
|
574
|
+
new CancelError({ cause: reason ?? new DOMException("Aborted", "AbortError") })
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
let entry = this.inFlight.get(key);
|
|
578
|
+
const isPrimary = !entry;
|
|
579
|
+
if (!entry) {
|
|
580
|
+
const underlyingAbortController = new AbortController();
|
|
581
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
582
|
+
const inFlightPromise = factory(underlyingAbortController.signal);
|
|
583
|
+
entry = {
|
|
584
|
+
key,
|
|
585
|
+
underlyingAbortController,
|
|
586
|
+
subscribers,
|
|
587
|
+
promise: inFlightPromise
|
|
588
|
+
};
|
|
589
|
+
this.inFlight.set(key, entry);
|
|
590
|
+
inFlightPromise.then((response) => {
|
|
591
|
+
this.inFlight.delete(key);
|
|
592
|
+
for (const sub of subscribers) {
|
|
593
|
+
sub.cleanup();
|
|
594
|
+
sub.resolve(response);
|
|
595
|
+
}
|
|
596
|
+
}).catch((error) => {
|
|
597
|
+
this.inFlight.delete(key);
|
|
598
|
+
for (const sub of subscribers) {
|
|
599
|
+
sub.cleanup();
|
|
600
|
+
sub.reject(error);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const currentEntry = entry;
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
const onAbort = () => {
|
|
607
|
+
cleanup();
|
|
608
|
+
const reason = callerSignal?.reason;
|
|
609
|
+
const cancelErr = reason instanceof CancelError ? reason : new CancelError({ cause: reason ?? new DOMException("Aborted", "AbortError") });
|
|
610
|
+
reject(cancelErr);
|
|
611
|
+
if (currentEntry.subscribers.size === 0) {
|
|
612
|
+
this.inFlight.delete(key);
|
|
613
|
+
currentEntry.underlyingAbortController.abort(
|
|
614
|
+
new DOMException("All subscribers aborted", "AbortError")
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const cleanup = () => {
|
|
619
|
+
if (callerSignal) {
|
|
620
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
621
|
+
}
|
|
622
|
+
currentEntry.subscribers.delete(subscriber);
|
|
623
|
+
};
|
|
624
|
+
const subscriber = {
|
|
625
|
+
resolve: (res) => {
|
|
626
|
+
cleanup();
|
|
627
|
+
resolve(isPrimary ? { ...res, fromDedup: false } : { ...res, fromDedup: true });
|
|
628
|
+
},
|
|
629
|
+
reject: (err) => {
|
|
630
|
+
cleanup();
|
|
631
|
+
const errObj = err instanceof Error ? err : new Error(typeof err === "string" ? err : "Unknown error");
|
|
632
|
+
reject(errObj);
|
|
633
|
+
},
|
|
634
|
+
cleanup
|
|
635
|
+
};
|
|
636
|
+
if (callerSignal) {
|
|
637
|
+
callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
638
|
+
}
|
|
639
|
+
currentEntry.subscribers.add(subscriber);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/** Returns the current number of active in-flight deduplicated requests. */
|
|
643
|
+
getActiveCount() {
|
|
644
|
+
return this.inFlight.size;
|
|
645
|
+
}
|
|
646
|
+
/** Clears all in-flight entries. */
|
|
647
|
+
clear() {
|
|
648
|
+
this.inFlight.clear();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/cache.ts
|
|
653
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
654
|
+
var DEFAULT_MAX_SIZE = 100;
|
|
655
|
+
var SAFE_CACHE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
656
|
+
function cloneData(data) {
|
|
657
|
+
if (data === null || typeof data !== "object") {
|
|
658
|
+
return data;
|
|
659
|
+
}
|
|
660
|
+
if (typeof structuredClone === "function") {
|
|
661
|
+
try {
|
|
662
|
+
return structuredClone(data);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
return JSON.parse(JSON.stringify(data));
|
|
668
|
+
} catch {
|
|
669
|
+
return data;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function generateCacheKey(method, url, headers, allowAuthenticated = false) {
|
|
673
|
+
const normMethod = method.toUpperCase();
|
|
674
|
+
const urlObj = new URL(url, "http://localhost");
|
|
675
|
+
urlObj.searchParams.sort();
|
|
676
|
+
const sortedUrl = urlObj.pathname + (urlObj.search ? urlObj.search : "");
|
|
677
|
+
let authPart = "";
|
|
678
|
+
if (allowAuthenticated && headers) {
|
|
679
|
+
const authHeader = headers["authorization"] ?? headers["Authorization"];
|
|
680
|
+
if (authHeader) {
|
|
681
|
+
authPart = `|auth:${authHeader}`;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return `${normMethod}:${sortedUrl}${authPart}`;
|
|
685
|
+
}
|
|
686
|
+
function isCacheEligible(method, headers, requestCache, clientCache) {
|
|
687
|
+
const normMethod = method.toUpperCase();
|
|
688
|
+
if (!SAFE_CACHE_METHODS.has(normMethod)) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
if (requestCache === false) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
if (typeof requestCache === "object" && requestCache.skip === true) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
const enabled = typeof requestCache === "object" && !requestCache.skip || requestCache === true || (clientCache?.enabled ?? false);
|
|
698
|
+
if (!enabled) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
const allowedMethods = clientCache?.methods ?? ["GET", "HEAD"];
|
|
702
|
+
if (!allowedMethods.includes(normMethod)) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const authHeader = headers ? headers["authorization"] ?? headers["Authorization"] : void 0;
|
|
706
|
+
if (authHeader && !(clientCache?.allowAuthenticated ?? false)) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
var CacheManager = class {
|
|
712
|
+
store = /* @__PURE__ */ new Map();
|
|
713
|
+
defaultTtl;
|
|
714
|
+
maxSize;
|
|
715
|
+
storage;
|
|
716
|
+
storageKey;
|
|
717
|
+
defaultStrategy;
|
|
718
|
+
constructor(config) {
|
|
719
|
+
this.defaultTtl = config?.defaultTtl ?? config?.ttl ?? DEFAULT_TTL_MS;
|
|
720
|
+
this.maxSize = config?.maxSize ?? DEFAULT_MAX_SIZE;
|
|
721
|
+
if (config?.storage !== void 0) {
|
|
722
|
+
this.storage = config.storage;
|
|
723
|
+
}
|
|
724
|
+
this.storageKey = config?.storageKey ?? "rnfc_http_cache";
|
|
725
|
+
this.defaultStrategy = config?.strategy ?? config?.defaultStrategy ?? "cache-first";
|
|
726
|
+
if (this.storage) {
|
|
727
|
+
void this.hydrate();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Hydrates cached entries from persistent storage into memory.
|
|
732
|
+
*/
|
|
733
|
+
async hydrate() {
|
|
734
|
+
if (!this.storage) return;
|
|
735
|
+
try {
|
|
736
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
737
|
+
if (!raw) return;
|
|
738
|
+
const parsed = JSON.parse(raw);
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
if (Array.isArray(parsed)) {
|
|
741
|
+
for (const [key, entry] of parsed) {
|
|
742
|
+
if (entry && typeof entry === "object" && entry.expiresAt > now) {
|
|
743
|
+
this.store.set(key, entry);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Persists active (non-expired) cache entries to storage adapter.
|
|
752
|
+
*/
|
|
753
|
+
async persist() {
|
|
754
|
+
if (!this.storage) return;
|
|
755
|
+
try {
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
const entries = [];
|
|
758
|
+
for (const [key, entry] of this.store.entries()) {
|
|
759
|
+
if (entry.expiresAt > now) {
|
|
760
|
+
entries.push([key, entry]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
await this.storage.setItem(this.storageKey, JSON.stringify(entries));
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Retrieves a cached response by key.
|
|
769
|
+
* If expired and allowStale is false, evicts the entry and returns undefined.
|
|
770
|
+
*/
|
|
771
|
+
get(key, options) {
|
|
772
|
+
const entry = this.store.get(key);
|
|
773
|
+
if (!entry) {
|
|
774
|
+
return void 0;
|
|
775
|
+
}
|
|
776
|
+
const isExpired = Date.now() > entry.expiresAt;
|
|
777
|
+
if (isExpired && !options?.allowStale) {
|
|
778
|
+
this.store.delete(key);
|
|
779
|
+
if (this.storage) void this.persist();
|
|
780
|
+
return void 0;
|
|
781
|
+
}
|
|
782
|
+
this.store.delete(key);
|
|
783
|
+
this.store.set(key, entry);
|
|
784
|
+
const res = {
|
|
785
|
+
...entry.response,
|
|
786
|
+
data: cloneData(entry.response.data),
|
|
787
|
+
cached: true
|
|
788
|
+
};
|
|
789
|
+
if (isExpired) {
|
|
790
|
+
res.stale = true;
|
|
791
|
+
}
|
|
792
|
+
return res;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Checks if an entry exists and is expired (stale).
|
|
796
|
+
*/
|
|
797
|
+
isStale(key) {
|
|
798
|
+
const entry = this.store.get(key);
|
|
799
|
+
if (!entry) return false;
|
|
800
|
+
return Date.now() > entry.expiresAt;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Retrieves the raw cache entry (including expired entries) for conditional revalidation.
|
|
804
|
+
*/
|
|
805
|
+
getEntry(key) {
|
|
806
|
+
return this.store.get(key);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Revalidates an existing cache entry on receiving an HTTP 304 Not Modified response.
|
|
810
|
+
* Updates headers, extends TTL, and returns the response with fresh cached data.
|
|
811
|
+
*/
|
|
812
|
+
updateFrom304(key, newHeaders, ttlMs) {
|
|
813
|
+
const entry = this.store.get(key);
|
|
814
|
+
if (!entry) return void 0;
|
|
815
|
+
const ttl = ttlMs !== void 0 && ttlMs >= 0 ? ttlMs : this.defaultTtl;
|
|
816
|
+
entry.expiresAt = Date.now() + ttl;
|
|
817
|
+
const mergedHeaders = { ...entry.response.headers, ...newHeaders };
|
|
818
|
+
entry.response = {
|
|
819
|
+
...entry.response,
|
|
820
|
+
headers: mergedHeaders
|
|
821
|
+
};
|
|
822
|
+
if (newHeaders["etag"] !== void 0) {
|
|
823
|
+
entry.etag = newHeaders["etag"];
|
|
824
|
+
}
|
|
825
|
+
if (newHeaders["last-modified"] !== void 0) {
|
|
826
|
+
entry.lastModified = newHeaders["last-modified"];
|
|
827
|
+
}
|
|
828
|
+
this.store.delete(key);
|
|
829
|
+
this.store.set(key, entry);
|
|
830
|
+
if (this.storage) void this.persist();
|
|
831
|
+
return {
|
|
832
|
+
...entry.response,
|
|
833
|
+
data: cloneData(entry.response.data),
|
|
834
|
+
cached: true,
|
|
835
|
+
stale: false
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
set(key, response, ttlMs) {
|
|
839
|
+
const ttl = ttlMs !== void 0 && ttlMs >= 0 ? ttlMs : this.defaultTtl;
|
|
840
|
+
const expiresAt = Date.now() + ttl;
|
|
841
|
+
const etag = response.headers["etag"];
|
|
842
|
+
const lastModified = response.headers["last-modified"];
|
|
843
|
+
const entry = {
|
|
844
|
+
response: {
|
|
845
|
+
...response,
|
|
846
|
+
data: cloneData(response.data),
|
|
847
|
+
cached: true
|
|
848
|
+
},
|
|
849
|
+
expiresAt
|
|
850
|
+
};
|
|
851
|
+
if (etag !== void 0) {
|
|
852
|
+
entry.etag = etag;
|
|
853
|
+
}
|
|
854
|
+
if (lastModified !== void 0) {
|
|
855
|
+
entry.lastModified = lastModified;
|
|
856
|
+
}
|
|
857
|
+
if (this.store.has(key)) {
|
|
858
|
+
this.store.delete(key);
|
|
859
|
+
} else if (this.store.size >= this.maxSize) {
|
|
860
|
+
const oldestKey = this.store.keys().next().value;
|
|
861
|
+
if (oldestKey !== void 0) {
|
|
862
|
+
this.store.delete(oldestKey);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
this.store.set(key, entry);
|
|
866
|
+
if (this.storage) void this.persist();
|
|
867
|
+
}
|
|
868
|
+
has(key) {
|
|
869
|
+
const entry = this.store.get(key);
|
|
870
|
+
if (!entry) return false;
|
|
871
|
+
if (Date.now() > entry.expiresAt) {
|
|
872
|
+
this.store.delete(key);
|
|
873
|
+
if (this.storage) void this.persist();
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
delete(key) {
|
|
879
|
+
const deleted = this.store.delete(key);
|
|
880
|
+
if (deleted && this.storage) void this.persist();
|
|
881
|
+
return deleted;
|
|
882
|
+
}
|
|
883
|
+
deletePattern(pattern) {
|
|
884
|
+
let deletedCount = 0;
|
|
885
|
+
const keysToDelete = [];
|
|
886
|
+
for (const key of this.store.keys()) {
|
|
887
|
+
if (typeof pattern === "string") {
|
|
888
|
+
if (key.includes(pattern)) {
|
|
889
|
+
keysToDelete.push(key);
|
|
890
|
+
}
|
|
891
|
+
} else if (pattern.test(key)) {
|
|
892
|
+
keysToDelete.push(key);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const key of keysToDelete) {
|
|
896
|
+
if (this.store.delete(key)) {
|
|
897
|
+
deletedCount++;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (deletedCount > 0 && this.storage) void this.persist();
|
|
901
|
+
return deletedCount;
|
|
902
|
+
}
|
|
903
|
+
clear() {
|
|
904
|
+
this.store.clear();
|
|
905
|
+
if (this.storage) void this.storage.removeItem(this.storageKey);
|
|
906
|
+
}
|
|
907
|
+
get size() {
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
for (const [key, entry] of this.store.entries()) {
|
|
910
|
+
if (now > entry.expiresAt) {
|
|
911
|
+
this.store.delete(key);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return this.store.size;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// src/validator.ts
|
|
919
|
+
function buildValidationOptions(data, config, issues, cause) {
|
|
920
|
+
const opts = { data };
|
|
921
|
+
if (config !== void 0) opts.config = config;
|
|
922
|
+
if (issues !== void 0) opts.issues = issues;
|
|
923
|
+
if (cause !== void 0) opts.cause = cause;
|
|
924
|
+
return opts;
|
|
925
|
+
}
|
|
926
|
+
async function validateData(schema, data, config) {
|
|
927
|
+
if (!schema) {
|
|
928
|
+
return data;
|
|
929
|
+
}
|
|
930
|
+
if (typeof schema === "object" && schema !== null && "~standard" in schema && typeof schema["~standard"]?.validate === "function") {
|
|
931
|
+
try {
|
|
932
|
+
const standard = schema["~standard"];
|
|
933
|
+
const result = await standard.validate(data);
|
|
934
|
+
if ("issues" in result && result.issues && result.issues.length > 0) {
|
|
935
|
+
const firstIssue = result.issues[0];
|
|
936
|
+
const message = firstIssue?.message ? `Schema validation failed: ${firstIssue.message}` : "Schema validation failed";
|
|
937
|
+
throw new ValidationError(message, buildValidationOptions(data, config, result.issues));
|
|
938
|
+
}
|
|
939
|
+
if ("value" in result) {
|
|
940
|
+
return result.value;
|
|
941
|
+
}
|
|
942
|
+
} catch (err) {
|
|
943
|
+
if (err instanceof ValidationError) throw err;
|
|
944
|
+
const msg = err instanceof Error ? err.message : "Schema validation failed";
|
|
945
|
+
throw new ValidationError(msg, buildValidationOptions(data, config, void 0, err));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (typeof schema === "object" && schema !== null && "safeParse" in schema && typeof schema.safeParse === "function") {
|
|
949
|
+
try {
|
|
950
|
+
const res = schema.safeParse(data);
|
|
951
|
+
if (!res.success) {
|
|
952
|
+
const issues = res.error && "issues" in res.error ? res.error.issues : void 0;
|
|
953
|
+
throw new ValidationError(
|
|
954
|
+
res.error?.message ?? "Schema validation failed",
|
|
955
|
+
buildValidationOptions(data, config, issues, res.error)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
return res.data;
|
|
959
|
+
} catch (err) {
|
|
960
|
+
if (err instanceof ValidationError) throw err;
|
|
961
|
+
const msg = err instanceof Error ? err.message : "Schema validation failed";
|
|
962
|
+
throw new ValidationError(msg, buildValidationOptions(data, config, void 0, err));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (typeof schema === "object" && schema !== null && "parse" in schema && typeof schema.parse === "function") {
|
|
966
|
+
try {
|
|
967
|
+
return schema.parse(data);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
const issues = err !== null && typeof err === "object" && "issues" in err ? err.issues : void 0;
|
|
970
|
+
const msg = err instanceof Error ? err.message : "Schema validation failed";
|
|
971
|
+
throw new ValidationError(msg, buildValidationOptions(data, config, issues, err));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (typeof schema === "function") {
|
|
975
|
+
try {
|
|
976
|
+
return await schema(data);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (err instanceof ValidationError) throw err;
|
|
979
|
+
const msg = err instanceof Error ? err.message : "Schema validation failed";
|
|
980
|
+
throw new ValidationError(msg, buildValidationOptions(data, config, void 0, err));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return data;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/circuit-breaker.ts
|
|
987
|
+
var DEFAULT_FAILURE_THRESHOLD = 5;
|
|
988
|
+
var DEFAULT_RESET_TIMEOUT_MS = 3e4;
|
|
989
|
+
function defaultIsFailure(err) {
|
|
990
|
+
if (isNetworkError(err) || isTimeoutError(err)) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
if (isHttpError(err) && err.status >= 500) {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
var CircuitBreaker = class {
|
|
999
|
+
constructor(config) {
|
|
1000
|
+
this.config = config;
|
|
1001
|
+
this.failureThreshold = config.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
1002
|
+
this.resetTimeoutMs = config.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
|
|
1003
|
+
this.isFailureFn = config.isFailure ?? defaultIsFailure;
|
|
1004
|
+
}
|
|
1005
|
+
config;
|
|
1006
|
+
state = "CLOSED";
|
|
1007
|
+
failureCount = 0;
|
|
1008
|
+
nextAttemptTime = 0;
|
|
1009
|
+
failureThreshold;
|
|
1010
|
+
resetTimeoutMs;
|
|
1011
|
+
isFailureFn;
|
|
1012
|
+
/**
|
|
1013
|
+
* Current lifecycle state of the circuit breaker.
|
|
1014
|
+
*/
|
|
1015
|
+
getState() {
|
|
1016
|
+
if (this.state === "OPEN" && Date.now() >= this.nextAttemptTime) {
|
|
1017
|
+
this.state = "HALF_OPEN";
|
|
1018
|
+
}
|
|
1019
|
+
return this.state;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Current consecutive failure count.
|
|
1023
|
+
*/
|
|
1024
|
+
getFailures() {
|
|
1025
|
+
return this.failureCount;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Resets the circuit breaker state to CLOSED and clears failure counts.
|
|
1029
|
+
*/
|
|
1030
|
+
reset() {
|
|
1031
|
+
this.state = "CLOSED";
|
|
1032
|
+
this.failureCount = 0;
|
|
1033
|
+
this.nextAttemptTime = 0;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Checks if a request is allowed to execute.
|
|
1037
|
+
* Throws `CircuitBreakerError` if the circuit is currently OPEN.
|
|
1038
|
+
*/
|
|
1039
|
+
beforeExecution() {
|
|
1040
|
+
const currentState = this.getState();
|
|
1041
|
+
if (currentState === "OPEN") {
|
|
1042
|
+
const remainingMs = Math.max(0, this.nextAttemptTime - Date.now());
|
|
1043
|
+
throw new CircuitBreakerError(
|
|
1044
|
+
`Circuit breaker is OPEN. Service is temporarily unavailable (retry in ${Math.round(remainingMs / 1e3)}s)`,
|
|
1045
|
+
{
|
|
1046
|
+
state: "OPEN",
|
|
1047
|
+
resetTimeoutMs: this.resetTimeoutMs
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Records a successful execution. If the circuit was in HALF_OPEN state,
|
|
1054
|
+
* it transitions back to CLOSED.
|
|
1055
|
+
*/
|
|
1056
|
+
recordSuccess() {
|
|
1057
|
+
this.failureCount = 0;
|
|
1058
|
+
this.state = "CLOSED";
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Records an execution error. Evaluates the failure predicate and
|
|
1062
|
+
* updates the circuit state accordingly.
|
|
1063
|
+
*/
|
|
1064
|
+
recordFailure(error) {
|
|
1065
|
+
if (!this.isFailureFn(error)) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const currentState = this.getState();
|
|
1069
|
+
if (currentState === "HALF_OPEN") {
|
|
1070
|
+
this.state = "OPEN";
|
|
1071
|
+
this.nextAttemptTime = Date.now() + this.resetTimeoutMs;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.failureCount++;
|
|
1075
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
1076
|
+
this.state = "OPEN";
|
|
1077
|
+
this.nextAttemptTime = Date.now() + this.resetTimeoutMs;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Executes a task wrapped in the circuit breaker lifecycle.
|
|
1082
|
+
*
|
|
1083
|
+
* @template T Return type of the task.
|
|
1084
|
+
* @param task Function returning a Promise to execute.
|
|
1085
|
+
* @returns Result of the task.
|
|
1086
|
+
* @throws {CircuitBreakerError} If the circuit is OPEN.
|
|
1087
|
+
*/
|
|
1088
|
+
async execute(task) {
|
|
1089
|
+
this.beforeExecution();
|
|
1090
|
+
try {
|
|
1091
|
+
const result = await task();
|
|
1092
|
+
this.recordSuccess();
|
|
1093
|
+
return result;
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
this.recordFailure(err);
|
|
1096
|
+
throw err;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// src/executor.ts
|
|
1102
|
+
var AuthRefreshError = class extends Error {
|
|
1103
|
+
constructor(originalError) {
|
|
1104
|
+
super("Auth refresh failed");
|
|
1105
|
+
this.originalError = originalError;
|
|
1106
|
+
this.name = "AuthRefreshError";
|
|
1107
|
+
}
|
|
1108
|
+
originalError;
|
|
1109
|
+
};
|
|
1110
|
+
async function dispatchFetch(url, method, headers, body, credentials, redirect, dispatcher, responseType, resolvedConfig, fetchImpl, signal, onDownloadProgress) {
|
|
1111
|
+
const reqInit = {
|
|
1112
|
+
method,
|
|
1113
|
+
headers
|
|
1114
|
+
};
|
|
1115
|
+
if (body !== null && body !== void 0) {
|
|
1116
|
+
reqInit.body = body;
|
|
1117
|
+
}
|
|
1118
|
+
if (credentials !== void 0) {
|
|
1119
|
+
reqInit.credentials = credentials;
|
|
1120
|
+
}
|
|
1121
|
+
if (redirect !== void 0) {
|
|
1122
|
+
reqInit.redirect = redirect;
|
|
1123
|
+
}
|
|
1124
|
+
if (dispatcher !== void 0) {
|
|
1125
|
+
reqInit.dispatcher = dispatcher;
|
|
1126
|
+
}
|
|
1127
|
+
reqInit.signal = signal;
|
|
1128
|
+
let requestObj;
|
|
1129
|
+
try {
|
|
1130
|
+
requestObj = new Request(url, reqInit);
|
|
1131
|
+
} catch {
|
|
1132
|
+
const fallbackUrl = /^https?:\/\//i.test(url) ? url : `http://localhost${url.startsWith("/") ? "" : "/"}${url}`;
|
|
1133
|
+
requestObj = new Request(fallbackUrl, reqInit);
|
|
1134
|
+
}
|
|
1135
|
+
const getAbortError = () => {
|
|
1136
|
+
const reason = signal.reason;
|
|
1137
|
+
if (reason instanceof Error) return reason;
|
|
1138
|
+
return new DOMException("Aborted", "AbortError");
|
|
1139
|
+
};
|
|
1140
|
+
let abortListener;
|
|
1141
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1142
|
+
if (signal.aborted) {
|
|
1143
|
+
reject(getAbortError());
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
abortListener = () => {
|
|
1147
|
+
reject(getAbortError());
|
|
1148
|
+
};
|
|
1149
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
1150
|
+
});
|
|
1151
|
+
let response;
|
|
1152
|
+
try {
|
|
1153
|
+
response = await Promise.race([fetchImpl(requestObj, reqInit), abortPromise]);
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
if (err instanceof TimeoutError) throw err;
|
|
1156
|
+
if (err instanceof CancelError) throw err;
|
|
1157
|
+
if (err instanceof Error && err.name === "AbortError" || signal.aborted) {
|
|
1158
|
+
const reason = signal.reason;
|
|
1159
|
+
if (reason instanceof TimeoutError) throw reason;
|
|
1160
|
+
if (reason instanceof CancelError) throw reason;
|
|
1161
|
+
throw new CancelError({ config: resolvedConfig, cause: reason ?? err });
|
|
1162
|
+
}
|
|
1163
|
+
const msg = err instanceof Error ? err.message : "Network request failed";
|
|
1164
|
+
throw new NetworkError(msg, { config: resolvedConfig, cause: err });
|
|
1165
|
+
} finally {
|
|
1166
|
+
if (abortListener) {
|
|
1167
|
+
signal.removeEventListener("abort", abortListener);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const resHeaders = {};
|
|
1171
|
+
response.headers.forEach((val, key) => {
|
|
1172
|
+
resHeaders[key.toLowerCase()] = val;
|
|
1173
|
+
});
|
|
1174
|
+
if (response.status === 304) {
|
|
1175
|
+
return {
|
|
1176
|
+
data: null,
|
|
1177
|
+
status: 304,
|
|
1178
|
+
statusText: response.statusText,
|
|
1179
|
+
headers: resHeaders,
|
|
1180
|
+
config: resolvedConfig,
|
|
1181
|
+
cached: true,
|
|
1182
|
+
fromDedup: false,
|
|
1183
|
+
request: requestObj
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (!response.ok) {
|
|
1187
|
+
const clonedResponse = response.clone();
|
|
1188
|
+
const errorData = await parseResponse(response, responseType, resolvedConfig).catch(() => null);
|
|
1189
|
+
throw new HttpError(`HTTP Error ${response.status} ${response.statusText}`, {
|
|
1190
|
+
status: response.status,
|
|
1191
|
+
statusText: response.statusText,
|
|
1192
|
+
data: errorData,
|
|
1193
|
+
headers: resHeaders,
|
|
1194
|
+
response: clonedResponse,
|
|
1195
|
+
config: resolvedConfig
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const data = await parseResponse(
|
|
1199
|
+
response,
|
|
1200
|
+
responseType,
|
|
1201
|
+
resolvedConfig,
|
|
1202
|
+
onDownloadProgress
|
|
1203
|
+
);
|
|
1204
|
+
return {
|
|
1205
|
+
data,
|
|
1206
|
+
status: response.status,
|
|
1207
|
+
statusText: response.statusText,
|
|
1208
|
+
headers: resHeaders,
|
|
1209
|
+
config: resolvedConfig,
|
|
1210
|
+
cached: false,
|
|
1211
|
+
fromDedup: false,
|
|
1212
|
+
request: requestObj
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async function executeRequest(config, clientConfig = {}, authManager, dedupManager, cacheManager, circuitBreaker, concurrencyLimiter) {
|
|
1216
|
+
if (concurrencyLimiter && !config._isFromConcurrency) {
|
|
1217
|
+
return concurrencyLimiter.run(
|
|
1218
|
+
() => executeRequest(
|
|
1219
|
+
{ ...config, _isFromConcurrency: true },
|
|
1220
|
+
clientConfig,
|
|
1221
|
+
authManager,
|
|
1222
|
+
dedupManager,
|
|
1223
|
+
cacheManager,
|
|
1224
|
+
circuitBreaker,
|
|
1225
|
+
concurrencyLimiter
|
|
1226
|
+
),
|
|
1227
|
+
config.signal
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const startTime = Date.now();
|
|
1231
|
+
const urlStr = config.url ?? "";
|
|
1232
|
+
const method = config.method ?? "GET";
|
|
1233
|
+
const baseURL = config.baseURL ?? clientConfig.baseURL;
|
|
1234
|
+
const params = { ...clientConfig.params, ...config.params };
|
|
1235
|
+
const pathParams = config.pathParams;
|
|
1236
|
+
const resolvedUrl = buildUrl(
|
|
1237
|
+
urlStr,
|
|
1238
|
+
baseURL,
|
|
1239
|
+
params,
|
|
1240
|
+
pathParams && Object.keys(pathParams).length > 0 ? pathParams : void 0
|
|
1241
|
+
);
|
|
1242
|
+
if (!resolvedUrl) {
|
|
1243
|
+
throw new ConfigError("URL is required");
|
|
1244
|
+
}
|
|
1245
|
+
const baseHeaders = mergeHeaders(clientConfig.headers, config.headers);
|
|
1246
|
+
const bearerSource = config.bearerToken ?? clientConfig.bearerToken;
|
|
1247
|
+
if (bearerSource && !baseHeaders["authorization"] && !baseHeaders["Authorization"]) {
|
|
1248
|
+
const tokenVal = typeof bearerSource === "function" ? await bearerSource() : bearerSource;
|
|
1249
|
+
if (tokenVal) {
|
|
1250
|
+
baseHeaders["authorization"] = `Bearer ${tokenVal}`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (authManager) {
|
|
1254
|
+
await authManager.attachToken(baseHeaders, resolvedUrl, config.auth);
|
|
1255
|
+
}
|
|
1256
|
+
const activeBreaker = config.circuitBreaker === false ? void 0 : typeof config.circuitBreaker === "object" ? new CircuitBreaker(config.circuitBreaker) : circuitBreaker;
|
|
1257
|
+
if (activeBreaker) {
|
|
1258
|
+
activeBreaker.beforeExecution();
|
|
1259
|
+
}
|
|
1260
|
+
const cacheEligible = cacheManager !== void 0 && isCacheEligible(method, baseHeaders, config.cache, clientConfig.cache);
|
|
1261
|
+
const cacheKey = cacheEligible ? generateCacheKey(method, resolvedUrl, baseHeaders, clientConfig.cache?.allowAuthenticated) : "";
|
|
1262
|
+
if (cacheEligible && cacheManager) {
|
|
1263
|
+
const cachedEntry = cacheManager.getEntry(cacheKey);
|
|
1264
|
+
const etagEnabled = typeof config.cache === "object" && config.cache.etag !== void 0 ? config.cache.etag : clientConfig.cache?.etag ?? true;
|
|
1265
|
+
if (cachedEntry) {
|
|
1266
|
+
const isFresh = Date.now() <= cachedEntry.expiresAt;
|
|
1267
|
+
const forceRevalidate = typeof config.cache === "object" && config.cache.etag === true;
|
|
1268
|
+
const strategy = typeof config.cache === "object" && config.cache.strategy || clientConfig.cache?.strategy || clientConfig.cache?.defaultStrategy || "cache-first";
|
|
1269
|
+
if (isFresh && !forceRevalidate) {
|
|
1270
|
+
const cachedResponse = cacheManager.get(cacheKey);
|
|
1271
|
+
if (cachedResponse !== void 0) {
|
|
1272
|
+
return cachedResponse;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (strategy === "stale-while-revalidate" && !config._isRevalidating) {
|
|
1276
|
+
const staleResponse = cacheManager.get(cacheKey, { allowStale: true });
|
|
1277
|
+
if (staleResponse !== void 0) {
|
|
1278
|
+
const revalidateConfig = {
|
|
1279
|
+
...config,
|
|
1280
|
+
_isRevalidating: true
|
|
1281
|
+
};
|
|
1282
|
+
void executeRequest(
|
|
1283
|
+
revalidateConfig,
|
|
1284
|
+
clientConfig,
|
|
1285
|
+
authManager,
|
|
1286
|
+
dedupManager,
|
|
1287
|
+
cacheManager,
|
|
1288
|
+
circuitBreaker,
|
|
1289
|
+
concurrencyLimiter
|
|
1290
|
+
).then((freshRes) => {
|
|
1291
|
+
const onRevalidated = typeof config.cache === "object" && config.cache?.onRevalidated || config.onRevalidated;
|
|
1292
|
+
if (onRevalidated) {
|
|
1293
|
+
onRevalidated(freshRes);
|
|
1294
|
+
}
|
|
1295
|
+
}).catch(() => {
|
|
1296
|
+
});
|
|
1297
|
+
return staleResponse;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (etagEnabled) {
|
|
1301
|
+
if (cachedEntry.etag && !baseHeaders["if-none-match"]) {
|
|
1302
|
+
baseHeaders["if-none-match"] = cachedEntry.etag;
|
|
1303
|
+
}
|
|
1304
|
+
if (cachedEntry.lastModified && !baseHeaders["if-modified-since"]) {
|
|
1305
|
+
baseHeaders["if-modified-since"] = cachedEntry.lastModified;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (dedupManager && !config._isFromDedup && isDedupEligible(method, config.dedup, clientConfig.dedup)) {
|
|
1311
|
+
const credentials2 = config.credentials ?? clientConfig.credentials;
|
|
1312
|
+
const dedupKey = generateDedupKey(method, resolvedUrl, baseHeaders, credentials2, config.body);
|
|
1313
|
+
return dedupManager.execute(
|
|
1314
|
+
dedupKey,
|
|
1315
|
+
(sharedSignal) => {
|
|
1316
|
+
return executeRequest(
|
|
1317
|
+
{ ...config, signal: sharedSignal, _isFromDedup: true },
|
|
1318
|
+
clientConfig,
|
|
1319
|
+
authManager,
|
|
1320
|
+
dedupManager,
|
|
1321
|
+
cacheManager,
|
|
1322
|
+
circuitBreaker,
|
|
1323
|
+
concurrencyLimiter
|
|
1324
|
+
);
|
|
1325
|
+
},
|
|
1326
|
+
config.signal
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const formUrlEncoded = config.formUrlEncoded ?? clientConfig.formUrlEncoded;
|
|
1330
|
+
const { body, contentType } = serializeBody(config.body, baseHeaders, formUrlEncoded);
|
|
1331
|
+
if (contentType && !baseHeaders["content-type"]) {
|
|
1332
|
+
baseHeaders["content-type"] = contentType;
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof FormData !== "undefined" && config.body instanceof FormData) {
|
|
1335
|
+
delete baseHeaders["content-type"];
|
|
1336
|
+
}
|
|
1337
|
+
const timeout = config.timeout ?? clientConfig.timeout ?? 0;
|
|
1338
|
+
const responseType = config.responseType ?? clientConfig.responseType ?? "json";
|
|
1339
|
+
const credentials = config.credentials ?? clientConfig.credentials;
|
|
1340
|
+
const redirect = config.redirect ?? clientConfig.redirect;
|
|
1341
|
+
const dispatcher = config.dispatcher ?? clientConfig.dispatcher;
|
|
1342
|
+
const onDownloadProgress = config.onDownloadProgress ?? clientConfig.onDownloadProgress;
|
|
1343
|
+
const onUploadProgress = config.onUploadProgress ?? clientConfig.onUploadProgress;
|
|
1344
|
+
const fetchImpl = config.fetchImpl ?? clientConfig.fetchImpl ?? globalThis.fetch;
|
|
1345
|
+
if (typeof fetchImpl !== "function") {
|
|
1346
|
+
throw new ConfigError(
|
|
1347
|
+
"globalThis.fetch is not available in this environment. react-native-fetch-client requires Node.js >= 18.0.0, a modern browser, or React Native. If you are running on Node.js 14 or 16, please upgrade to Node.js >= 18 or provide a fetch polyfill via the `fetchImpl` option (e.g. `createClient({ fetchImpl: nodeFetch })`)."
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const resolvedRetry = resolveRetryPolicy(clientConfig.retry, config.retry);
|
|
1351
|
+
const resolvedConfig = {
|
|
1352
|
+
url: resolvedUrl,
|
|
1353
|
+
method,
|
|
1354
|
+
headers: { ...baseHeaders },
|
|
1355
|
+
timeout,
|
|
1356
|
+
responseType,
|
|
1357
|
+
retry: resolvedRetry
|
|
1358
|
+
};
|
|
1359
|
+
if (Object.keys(params).length > 0) {
|
|
1360
|
+
resolvedConfig.params = params;
|
|
1361
|
+
}
|
|
1362
|
+
if (pathParams && Object.keys(pathParams).length > 0) {
|
|
1363
|
+
resolvedConfig.pathParams = pathParams;
|
|
1364
|
+
}
|
|
1365
|
+
if (credentials !== void 0) {
|
|
1366
|
+
resolvedConfig.credentials = credentials;
|
|
1367
|
+
}
|
|
1368
|
+
const authHeaderKey = authManager ? authManager.getHeaderName() : "authorization";
|
|
1369
|
+
if (resolvedConfig.headers[authHeaderKey]) {
|
|
1370
|
+
resolvedConfig.headers[authHeaderKey] = "[REDACTED]";
|
|
1371
|
+
}
|
|
1372
|
+
if (resolvedConfig.headers["authorization"]) {
|
|
1373
|
+
resolvedConfig.headers["authorization"] = "[REDACTED]";
|
|
1374
|
+
}
|
|
1375
|
+
let totalTimerId;
|
|
1376
|
+
let onCallerAbort;
|
|
1377
|
+
const totalController = new AbortController();
|
|
1378
|
+
if (timeout > 0) {
|
|
1379
|
+
totalTimerId = setTimeout(() => {
|
|
1380
|
+
totalController.abort(new TimeoutError(timeout, { config: resolvedConfig }));
|
|
1381
|
+
}, timeout);
|
|
1382
|
+
}
|
|
1383
|
+
if (config.signal) {
|
|
1384
|
+
if (config.signal.aborted) {
|
|
1385
|
+
const reason = config.signal.reason;
|
|
1386
|
+
if (reason instanceof CancelError) throw reason;
|
|
1387
|
+
throw new CancelError({ config: resolvedConfig, cause: reason });
|
|
1388
|
+
}
|
|
1389
|
+
onCallerAbort = () => {
|
|
1390
|
+
const reason = config.signal?.reason;
|
|
1391
|
+
totalController.abort(
|
|
1392
|
+
reason instanceof Error ? reason : new CancelError({ config: resolvedConfig, cause: reason })
|
|
1393
|
+
);
|
|
1394
|
+
};
|
|
1395
|
+
config.signal.addEventListener("abort", onCallerAbort);
|
|
1396
|
+
}
|
|
1397
|
+
const cleanup = () => {
|
|
1398
|
+
if (totalTimerId !== void 0) {
|
|
1399
|
+
clearTimeout(totalTimerId);
|
|
1400
|
+
}
|
|
1401
|
+
if (onCallerAbort && config.signal) {
|
|
1402
|
+
config.signal.removeEventListener("abort", onCallerAbort);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
if (onUploadProgress && body !== null && body !== void 0) {
|
|
1406
|
+
let size = 0;
|
|
1407
|
+
if (typeof body === "string") {
|
|
1408
|
+
size = new TextEncoder().encode(body).length;
|
|
1409
|
+
} else if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
1410
|
+
size = body.size;
|
|
1411
|
+
} else if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
|
|
1412
|
+
size = body.byteLength;
|
|
1413
|
+
} else if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
1414
|
+
size = 1;
|
|
1415
|
+
try {
|
|
1416
|
+
let count = 0;
|
|
1417
|
+
body.forEach((val) => {
|
|
1418
|
+
if (typeof val === "string") {
|
|
1419
|
+
count += val.length;
|
|
1420
|
+
} else if (val && typeof val.size === "number") {
|
|
1421
|
+
count += val.size;
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
if (count > 0) size = count;
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (size > 0) {
|
|
1429
|
+
onUploadProgress({
|
|
1430
|
+
loaded: size,
|
|
1431
|
+
total: size,
|
|
1432
|
+
progress: 1,
|
|
1433
|
+
bytes: size
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const debugOpt = config.debug ?? clientConfig.debug;
|
|
1438
|
+
const logSuccess = (res) => {
|
|
1439
|
+
if (!debugOpt) return;
|
|
1440
|
+
const durationMs = Date.now() - startTime;
|
|
1441
|
+
const entry = {
|
|
1442
|
+
method,
|
|
1443
|
+
url: resolvedUrl,
|
|
1444
|
+
status: res.status,
|
|
1445
|
+
durationMs,
|
|
1446
|
+
headers: resolvedConfig.headers,
|
|
1447
|
+
cached: res.cached,
|
|
1448
|
+
fromDedup: res.fromDedup
|
|
1449
|
+
};
|
|
1450
|
+
if (typeof debugOpt === "function") {
|
|
1451
|
+
debugOpt(entry);
|
|
1452
|
+
} else {
|
|
1453
|
+
const tags = (res.cached ? " [CACHED]" : "") + (res.fromDedup ? " [DEDUP]" : "");
|
|
1454
|
+
console.log(
|
|
1455
|
+
`[react-native-fetch-client] ${method} ${resolvedUrl} -> ${res.status} ${res.statusText} (${durationMs}ms)${tags}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
const logError = (err) => {
|
|
1460
|
+
if (!debugOpt) return;
|
|
1461
|
+
const durationMs = Date.now() - startTime;
|
|
1462
|
+
const entry = {
|
|
1463
|
+
method,
|
|
1464
|
+
url: resolvedUrl,
|
|
1465
|
+
durationMs,
|
|
1466
|
+
headers: resolvedConfig.headers,
|
|
1467
|
+
error: err
|
|
1468
|
+
};
|
|
1469
|
+
if (typeof debugOpt === "function") {
|
|
1470
|
+
debugOpt(entry);
|
|
1471
|
+
} else {
|
|
1472
|
+
console.warn(
|
|
1473
|
+
`[react-native-fetch-client] ${method} ${resolvedUrl} -> ERROR (${durationMs}ms):`,
|
|
1474
|
+
err instanceof Error ? err.message : String(err)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
let attempt = 0;
|
|
1479
|
+
let lastError;
|
|
1480
|
+
try {
|
|
1481
|
+
while (true) {
|
|
1482
|
+
attempt++;
|
|
1483
|
+
try {
|
|
1484
|
+
let response = await dispatchFetch(
|
|
1485
|
+
resolvedUrl,
|
|
1486
|
+
method,
|
|
1487
|
+
baseHeaders,
|
|
1488
|
+
body,
|
|
1489
|
+
credentials,
|
|
1490
|
+
redirect,
|
|
1491
|
+
dispatcher,
|
|
1492
|
+
responseType,
|
|
1493
|
+
resolvedConfig,
|
|
1494
|
+
fetchImpl,
|
|
1495
|
+
totalController.signal,
|
|
1496
|
+
onDownloadProgress
|
|
1497
|
+
);
|
|
1498
|
+
cleanup();
|
|
1499
|
+
if (response.status === 304 && cacheManager && cacheKey) {
|
|
1500
|
+
const reqTtl = typeof config.cache === "object" && config.cache.ttl !== void 0 ? config.cache.ttl : void 0;
|
|
1501
|
+
const updated = cacheManager.updateFrom304(cacheKey, response.headers, reqTtl);
|
|
1502
|
+
if (updated) {
|
|
1503
|
+
response = updated;
|
|
1504
|
+
}
|
|
1505
|
+
} else if (cacheEligible && cacheManager) {
|
|
1506
|
+
const reqTtl = typeof config.cache === "object" && config.cache.ttl !== void 0 ? config.cache.ttl : void 0;
|
|
1507
|
+
cacheManager.set(cacheKey, response, reqTtl);
|
|
1508
|
+
}
|
|
1509
|
+
if (cacheManager && ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
1510
|
+
const urlObj = new URL(resolvedUrl, "http://localhost");
|
|
1511
|
+
cacheManager.deletePattern(urlObj.pathname);
|
|
1512
|
+
}
|
|
1513
|
+
if (config.schema) {
|
|
1514
|
+
const validatedData = await validateData(
|
|
1515
|
+
config.schema,
|
|
1516
|
+
response.data,
|
|
1517
|
+
resolvedConfig
|
|
1518
|
+
);
|
|
1519
|
+
response = {
|
|
1520
|
+
...response,
|
|
1521
|
+
data: validatedData
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
activeBreaker?.recordSuccess();
|
|
1525
|
+
logSuccess(response);
|
|
1526
|
+
return response;
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
lastError = err;
|
|
1529
|
+
if (authManager && authManager.isEligibleForRefresh(err, resolvedUrl, config._isRefreshed, config.auth)) {
|
|
1530
|
+
cleanup();
|
|
1531
|
+
try {
|
|
1532
|
+
await authManager.waitForRefresh(totalController.signal);
|
|
1533
|
+
const retriedConfig = {
|
|
1534
|
+
...config,
|
|
1535
|
+
_isRefreshed: true
|
|
1536
|
+
};
|
|
1537
|
+
return await executeRequest(
|
|
1538
|
+
retriedConfig,
|
|
1539
|
+
clientConfig,
|
|
1540
|
+
authManager,
|
|
1541
|
+
dedupManager,
|
|
1542
|
+
cacheManager,
|
|
1543
|
+
circuitBreaker,
|
|
1544
|
+
concurrencyLimiter
|
|
1545
|
+
);
|
|
1546
|
+
} catch (refreshErr) {
|
|
1547
|
+
if (totalController.signal.aborted) {
|
|
1548
|
+
const reason = totalController.signal.reason;
|
|
1549
|
+
if (reason instanceof TimeoutError) throw reason;
|
|
1550
|
+
if (reason instanceof CancelError) throw reason;
|
|
1551
|
+
throw new CancelError({ config: resolvedConfig, cause: reason });
|
|
1552
|
+
}
|
|
1553
|
+
throw new AuthRefreshError(refreshErr);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (shouldRetry(err, attempt, resolvedConfig)) {
|
|
1557
|
+
const delayMs = calculateRetryDelay(attempt, resolvedConfig.retry, err);
|
|
1558
|
+
try {
|
|
1559
|
+
await sleep(delayMs, totalController.signal);
|
|
1560
|
+
continue;
|
|
1561
|
+
} catch (sleepErr) {
|
|
1562
|
+
if (totalController.signal.aborted) {
|
|
1563
|
+
const reason = totalController.signal.reason;
|
|
1564
|
+
if (reason instanceof TimeoutError) throw reason;
|
|
1565
|
+
if (reason instanceof CancelError) throw reason;
|
|
1566
|
+
throw new CancelError({ config: resolvedConfig, cause: reason });
|
|
1567
|
+
}
|
|
1568
|
+
throw sleepErr;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (attempt > 1) {
|
|
1572
|
+
throw new RetryExhaustedError(attempt, lastError, { config: resolvedConfig });
|
|
1573
|
+
}
|
|
1574
|
+
throw lastError;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
cleanup();
|
|
1579
|
+
activeBreaker?.recordFailure(err);
|
|
1580
|
+
logError(err);
|
|
1581
|
+
if (err instanceof AuthRefreshError) throw err.originalError;
|
|
1582
|
+
if (err instanceof TimeoutError) throw err;
|
|
1583
|
+
if (err instanceof CancelError) throw err;
|
|
1584
|
+
if (err instanceof RetryExhaustedError) throw err;
|
|
1585
|
+
if (err instanceof HttpError) throw err;
|
|
1586
|
+
if (err instanceof ConfigError) throw err;
|
|
1587
|
+
if (err instanceof ParseError) throw err;
|
|
1588
|
+
if (err instanceof ValidationError) throw err;
|
|
1589
|
+
if (err instanceof CircuitBreakerError) throw err;
|
|
1590
|
+
if (err instanceof NetworkError) throw err;
|
|
1591
|
+
if (err instanceof Error && err.name === "AbortError" || totalController.signal.aborted) {
|
|
1592
|
+
const reason = totalController.signal.reason;
|
|
1593
|
+
if (reason instanceof TimeoutError) throw reason;
|
|
1594
|
+
if (reason instanceof CancelError) throw reason;
|
|
1595
|
+
throw new CancelError({ config: resolvedConfig, cause: reason ?? err });
|
|
1596
|
+
}
|
|
1597
|
+
const msg = err instanceof Error ? err.message : "Network request failed";
|
|
1598
|
+
throw new NetworkError(msg, { config: resolvedConfig, cause: err });
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/auth.ts
|
|
1603
|
+
var AuthManager = class {
|
|
1604
|
+
constructor(config) {
|
|
1605
|
+
this.config = config;
|
|
1606
|
+
}
|
|
1607
|
+
config;
|
|
1608
|
+
activeRefreshPromise = null;
|
|
1609
|
+
/** Returns the configured header name in lowercase, defaulting to 'authorization'. */
|
|
1610
|
+
getHeaderName() {
|
|
1611
|
+
return (this.config?.headerName || "Authorization").toLowerCase();
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Attaches the access token retrieved from `getToken()` to request headers.
|
|
1615
|
+
* Does not overwrite an existing explicit header of the same name.
|
|
1616
|
+
*/
|
|
1617
|
+
async attachToken(headers, _url, requestAuth) {
|
|
1618
|
+
if (!this.config?.getToken) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (requestAuth === false || typeof requestAuth === "object" && requestAuth.skip) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const token = await this.config.getToken();
|
|
1625
|
+
if (!token) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const headerKey = this.getHeaderName();
|
|
1629
|
+
if (!headers[headerKey]) {
|
|
1630
|
+
const prefix = this.config.tokenPrefix !== void 0 ? this.config.tokenPrefix : "Bearer ";
|
|
1631
|
+
headers[headerKey] = `${prefix}${token}`;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Checks whether an error from a request URL is eligible for a token refresh attempt.
|
|
1636
|
+
*/
|
|
1637
|
+
isEligibleForRefresh(error, url, isRefreshed, requestAuth) {
|
|
1638
|
+
if (!this.config?.refreshToken) {
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
if (isRefreshed) {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
if (requestAuth === false || typeof requestAuth === "object" && requestAuth.skip) {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
if (!(error instanceof HttpError)) {
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
const refreshStatuses = this.config.refreshOn ?? [401];
|
|
1651
|
+
if (!refreshStatuses.includes(error.status)) {
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
if (this.isExcludedUrl(url)) {
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Initiates or shares an in-flight single-flight token refresh operation.
|
|
1661
|
+
* Concurrent 401s await the same Promise. Cleared on completion or error.
|
|
1662
|
+
*/
|
|
1663
|
+
refresh() {
|
|
1664
|
+
const refreshTokenFn = this.config?.refreshToken;
|
|
1665
|
+
if (!refreshTokenFn) {
|
|
1666
|
+
return Promise.reject(new Error("refreshToken callback is not configured"));
|
|
1667
|
+
}
|
|
1668
|
+
if (this.activeRefreshPromise) {
|
|
1669
|
+
return this.activeRefreshPromise;
|
|
1670
|
+
}
|
|
1671
|
+
this.activeRefreshPromise = Promise.resolve().then(() => refreshTokenFn()).finally(() => {
|
|
1672
|
+
this.activeRefreshPromise = null;
|
|
1673
|
+
});
|
|
1674
|
+
return this.activeRefreshPromise;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Waits for token refresh while respecting the caller's request cancellation signal.
|
|
1678
|
+
*/
|
|
1679
|
+
waitForRefresh(signal) {
|
|
1680
|
+
if (signal?.aborted) {
|
|
1681
|
+
const reason = signal.reason;
|
|
1682
|
+
if (reason instanceof Error) {
|
|
1683
|
+
return Promise.reject(reason);
|
|
1684
|
+
}
|
|
1685
|
+
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
1686
|
+
}
|
|
1687
|
+
const refreshPromise = this.refresh();
|
|
1688
|
+
if (!signal) {
|
|
1689
|
+
return refreshPromise;
|
|
1690
|
+
}
|
|
1691
|
+
let abortHandler;
|
|
1692
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1693
|
+
abortHandler = () => {
|
|
1694
|
+
const reason = signal.reason;
|
|
1695
|
+
if (reason instanceof Error) {
|
|
1696
|
+
reject(reason);
|
|
1697
|
+
} else {
|
|
1698
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1702
|
+
});
|
|
1703
|
+
return Promise.race([refreshPromise, abortPromise]).finally(() => {
|
|
1704
|
+
if (abortHandler) {
|
|
1705
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
isExcludedUrl(url) {
|
|
1710
|
+
if (!this.config?.refreshExcludeUrls || this.config.refreshExcludeUrls.length === 0) {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
return this.config.refreshExcludeUrls.some((entry) => {
|
|
1714
|
+
if (typeof entry === "string") {
|
|
1715
|
+
return url === entry || url.includes(entry);
|
|
1716
|
+
}
|
|
1717
|
+
return entry.test(url);
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// src/concurrency.ts
|
|
1723
|
+
var ConcurrencyLimiter = class {
|
|
1724
|
+
/**
|
|
1725
|
+
* @param maxConcurrent Maximum number of concurrent tasks allowed to run simultaneously.
|
|
1726
|
+
* If less than or equal to 0, concurrency is unlimited.
|
|
1727
|
+
*/
|
|
1728
|
+
constructor(maxConcurrent) {
|
|
1729
|
+
this.maxConcurrent = maxConcurrent;
|
|
1730
|
+
}
|
|
1731
|
+
maxConcurrent;
|
|
1732
|
+
activeCount = 0;
|
|
1733
|
+
queue = [];
|
|
1734
|
+
/**
|
|
1735
|
+
* Acquires a concurrency slot. Resolves immediately if active count < limit,
|
|
1736
|
+
* otherwise blocks until a preceding task releases a slot.
|
|
1737
|
+
*/
|
|
1738
|
+
async acquire(signal) {
|
|
1739
|
+
if (this.maxConcurrent <= 0 || this.activeCount < this.maxConcurrent) {
|
|
1740
|
+
this.activeCount++;
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
return new Promise((resolve, reject) => {
|
|
1744
|
+
const toError = (e, fallback) => {
|
|
1745
|
+
if (e instanceof Error) return e;
|
|
1746
|
+
return new Error(typeof e === "string" ? e : fallback);
|
|
1747
|
+
};
|
|
1748
|
+
let onAbort;
|
|
1749
|
+
const item = {
|
|
1750
|
+
resolve: () => {
|
|
1751
|
+
if (signal && onAbort) {
|
|
1752
|
+
signal.removeEventListener("abort", onAbort);
|
|
1753
|
+
}
|
|
1754
|
+
this.activeCount++;
|
|
1755
|
+
resolve();
|
|
1756
|
+
},
|
|
1757
|
+
reject: (err) => {
|
|
1758
|
+
if (signal && onAbort) {
|
|
1759
|
+
signal.removeEventListener("abort", onAbort);
|
|
1760
|
+
}
|
|
1761
|
+
reject(toError(err, "Concurrency task failed"));
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
if (signal) {
|
|
1765
|
+
if (signal.aborted) {
|
|
1766
|
+
reject(toError(signal.reason, "Request was aborted while queued"));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
onAbort = () => {
|
|
1770
|
+
const idx = this.queue.indexOf(item);
|
|
1771
|
+
if (idx !== -1) {
|
|
1772
|
+
this.queue.splice(idx, 1);
|
|
1773
|
+
}
|
|
1774
|
+
reject(toError(signal.reason, "Request was aborted while queued"));
|
|
1775
|
+
};
|
|
1776
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1777
|
+
}
|
|
1778
|
+
this.queue.push(item);
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Releases an active concurrency slot and unblocks the next queued task in FIFO order.
|
|
1783
|
+
*/
|
|
1784
|
+
release() {
|
|
1785
|
+
this.activeCount--;
|
|
1786
|
+
if (this.queue.length > 0 && this.activeCount < this.maxConcurrent) {
|
|
1787
|
+
const next = this.queue.shift();
|
|
1788
|
+
if (next) {
|
|
1789
|
+
next.resolve();
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Schedules an asynchronous function through the concurrency limiter.
|
|
1795
|
+
*
|
|
1796
|
+
* @template T The return type of the function.
|
|
1797
|
+
* @param task Function returning a Promise to execute once a slot is available.
|
|
1798
|
+
* @param signal Optional AbortSignal to cancel waiting in the queue.
|
|
1799
|
+
* @returns The resolved value of the task.
|
|
1800
|
+
*/
|
|
1801
|
+
async run(task, signal) {
|
|
1802
|
+
if (this.maxConcurrent <= 0) {
|
|
1803
|
+
return task();
|
|
1804
|
+
}
|
|
1805
|
+
await this.acquire(signal);
|
|
1806
|
+
try {
|
|
1807
|
+
return await task();
|
|
1808
|
+
} finally {
|
|
1809
|
+
this.release();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Current number of actively executing requests.
|
|
1814
|
+
*/
|
|
1815
|
+
get active() {
|
|
1816
|
+
return this.activeCount;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Current number of requests queued waiting for an available slot.
|
|
1820
|
+
*/
|
|
1821
|
+
get pending() {
|
|
1822
|
+
return this.queue.length;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Clears all queued pending requests and rejects them with the provided error.
|
|
1826
|
+
*/
|
|
1827
|
+
clear(error = new Error("Concurrency queue was cancelled")) {
|
|
1828
|
+
while (this.queue.length > 0) {
|
|
1829
|
+
const item = this.queue.shift();
|
|
1830
|
+
item?.reject(error);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// src/stream.ts
|
|
1836
|
+
async function* parseSseStream(stream, options) {
|
|
1837
|
+
const reader = stream.getReader();
|
|
1838
|
+
const decoder = new TextDecoder();
|
|
1839
|
+
let buffer = "";
|
|
1840
|
+
let currentEvent;
|
|
1841
|
+
let currentData = [];
|
|
1842
|
+
let currentId;
|
|
1843
|
+
let currentRetry;
|
|
1844
|
+
const rawLines = options?.rawLines ?? false;
|
|
1845
|
+
try {
|
|
1846
|
+
while (true) {
|
|
1847
|
+
const { done, value } = await reader.read();
|
|
1848
|
+
if (done) {
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1852
|
+
const lines = buffer.split(/\r\n|\r|\n/);
|
|
1853
|
+
buffer = lines.pop() ?? "";
|
|
1854
|
+
for (const line of lines) {
|
|
1855
|
+
if (rawLines) {
|
|
1856
|
+
if (line.trim().length > 0) {
|
|
1857
|
+
yield { data: line };
|
|
1858
|
+
}
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (line.trim() === "") {
|
|
1862
|
+
if (currentData.length > 0 || currentEvent !== void 0 || currentId !== void 0) {
|
|
1863
|
+
const eventPayload = {
|
|
1864
|
+
data: currentData.join("\n")
|
|
1865
|
+
};
|
|
1866
|
+
if (currentEvent !== void 0) {
|
|
1867
|
+
eventPayload.event = currentEvent;
|
|
1868
|
+
}
|
|
1869
|
+
if (currentId !== void 0) {
|
|
1870
|
+
eventPayload.id = currentId;
|
|
1871
|
+
}
|
|
1872
|
+
if (currentRetry !== void 0) {
|
|
1873
|
+
eventPayload.retry = currentRetry;
|
|
1874
|
+
}
|
|
1875
|
+
yield eventPayload;
|
|
1876
|
+
currentEvent = void 0;
|
|
1877
|
+
currentData = [];
|
|
1878
|
+
currentId = void 0;
|
|
1879
|
+
currentRetry = void 0;
|
|
1880
|
+
}
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (line.startsWith(":")) {
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
let field = line;
|
|
1887
|
+
let valueStr = "";
|
|
1888
|
+
const colonIdx = line.indexOf(":");
|
|
1889
|
+
if (colonIdx !== -1) {
|
|
1890
|
+
field = line.substring(0, colonIdx);
|
|
1891
|
+
valueStr = line.substring(colonIdx + 1);
|
|
1892
|
+
if (valueStr.startsWith(" ")) {
|
|
1893
|
+
valueStr = valueStr.substring(1);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
switch (field) {
|
|
1897
|
+
case "event":
|
|
1898
|
+
currentEvent = valueStr;
|
|
1899
|
+
break;
|
|
1900
|
+
case "data":
|
|
1901
|
+
currentData.push(valueStr);
|
|
1902
|
+
break;
|
|
1903
|
+
case "id":
|
|
1904
|
+
currentId = valueStr;
|
|
1905
|
+
break;
|
|
1906
|
+
case "retry": {
|
|
1907
|
+
const parsed = parseInt(valueStr, 10);
|
|
1908
|
+
if (!Number.isNaN(parsed)) {
|
|
1909
|
+
currentRetry = parsed;
|
|
1910
|
+
}
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (buffer.trim().length > 0) {
|
|
1917
|
+
const line = buffer;
|
|
1918
|
+
if (rawLines) {
|
|
1919
|
+
yield { data: line };
|
|
1920
|
+
} else if (!line.startsWith(":")) {
|
|
1921
|
+
let field = line;
|
|
1922
|
+
let valueStr = "";
|
|
1923
|
+
const colonIdx = line.indexOf(":");
|
|
1924
|
+
if (colonIdx !== -1) {
|
|
1925
|
+
field = line.substring(0, colonIdx);
|
|
1926
|
+
valueStr = line.substring(colonIdx + 1);
|
|
1927
|
+
if (valueStr.startsWith(" ")) {
|
|
1928
|
+
valueStr = valueStr.substring(1);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (field === "data") {
|
|
1932
|
+
currentData.push(valueStr);
|
|
1933
|
+
} else if (field === "event") {
|
|
1934
|
+
currentEvent = valueStr;
|
|
1935
|
+
} else if (field === "id") {
|
|
1936
|
+
currentId = valueStr;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (!rawLines && (currentData.length > 0 || currentEvent !== void 0)) {
|
|
1941
|
+
const finalPayload = {
|
|
1942
|
+
data: currentData.join("\n")
|
|
1943
|
+
};
|
|
1944
|
+
if (currentEvent !== void 0) {
|
|
1945
|
+
finalPayload.event = currentEvent;
|
|
1946
|
+
}
|
|
1947
|
+
if (currentId !== void 0) {
|
|
1948
|
+
finalPayload.id = currentId;
|
|
1949
|
+
}
|
|
1950
|
+
if (currentRetry !== void 0) {
|
|
1951
|
+
finalPayload.retry = currentRetry;
|
|
1952
|
+
}
|
|
1953
|
+
yield finalPayload;
|
|
1954
|
+
}
|
|
1955
|
+
} finally {
|
|
1956
|
+
reader.releaseLock();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/interceptor.ts
|
|
1961
|
+
var DefaultInterceptorManager = class {
|
|
1962
|
+
handlers = [];
|
|
1963
|
+
nextId = 0;
|
|
1964
|
+
/**
|
|
1965
|
+
* Register a new interceptor.
|
|
1966
|
+
* @returns Numeric registration ID used to eject.
|
|
1967
|
+
*/
|
|
1968
|
+
use(fulfilled, rejected) {
|
|
1969
|
+
const id = this.nextId++;
|
|
1970
|
+
const entry = {
|
|
1971
|
+
id,
|
|
1972
|
+
interceptor: {
|
|
1973
|
+
fulfilled
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
if (rejected !== void 0) {
|
|
1977
|
+
entry.interceptor.rejected = rejected;
|
|
1978
|
+
}
|
|
1979
|
+
this.handlers.push(entry);
|
|
1980
|
+
return id;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Remove a registered interceptor by ID.
|
|
1984
|
+
*/
|
|
1985
|
+
eject(id) {
|
|
1986
|
+
const index = this.handlers.findIndex((h) => h !== null && h.id === id);
|
|
1987
|
+
if (index !== -1) {
|
|
1988
|
+
this.handlers[index] = null;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Remove all registered interceptors.
|
|
1993
|
+
*/
|
|
1994
|
+
clear() {
|
|
1995
|
+
this.handlers = [];
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Returns active interceptors in registration order.
|
|
1999
|
+
*/
|
|
2000
|
+
getInterceptors() {
|
|
2001
|
+
const active = [];
|
|
2002
|
+
for (const h of this.handlers) {
|
|
2003
|
+
if (h !== null) {
|
|
2004
|
+
active.push(h.interceptor);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return active;
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// src/paginate.ts
|
|
2012
|
+
function parseNextLinkHeader(header) {
|
|
2013
|
+
if (!header) return null;
|
|
2014
|
+
const parts = header.split(",");
|
|
2015
|
+
for (const part of parts) {
|
|
2016
|
+
const section = part.split(";");
|
|
2017
|
+
if (section.length >= 2) {
|
|
2018
|
+
const urlMatch = section[0]?.trim().match(/^<([^>]+)>/);
|
|
2019
|
+
const relMatch = section[1]?.trim().match(/rel=["']?([^"']+)["']?/i);
|
|
2020
|
+
if (urlMatch && relMatch && relMatch[1]?.toLowerCase() === "next") {
|
|
2021
|
+
return urlMatch[1] ?? null;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
function defaultExtractItems(data) {
|
|
2028
|
+
if (Array.isArray(data)) {
|
|
2029
|
+
return data;
|
|
2030
|
+
}
|
|
2031
|
+
if (data && typeof data === "object") {
|
|
2032
|
+
const obj = data;
|
|
2033
|
+
for (const key of ["items", "data", "results", "records", "rows", "elements"]) {
|
|
2034
|
+
if (Array.isArray(obj[key])) {
|
|
2035
|
+
return obj[key];
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
for (const val of Object.values(obj)) {
|
|
2039
|
+
if (Array.isArray(val)) {
|
|
2040
|
+
return val;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return [];
|
|
2045
|
+
}
|
|
2046
|
+
function createPaginateHelper(client) {
|
|
2047
|
+
async function* pagesGenerator(initialUrl, options) {
|
|
2048
|
+
const cfg = options?.pagination ?? {};
|
|
2049
|
+
const strategy = cfg.type ?? "page";
|
|
2050
|
+
const pageParam = cfg.pageParam ?? cfg.param ?? "page";
|
|
2051
|
+
const pageSizeParam = cfg.pageSizeParam ?? cfg.limitParam ?? "limit";
|
|
2052
|
+
const pageSize = cfg.pageSize ?? cfg.limit;
|
|
2053
|
+
const offsetParam = cfg.offsetParam ?? "offset";
|
|
2054
|
+
const cursorParam = cfg.cursorParam ?? "cursor";
|
|
2055
|
+
const maxPages = cfg.maxPages ?? cfg.totalPages ?? 50;
|
|
2056
|
+
let currentPage = cfg.initialPage ?? 1;
|
|
2057
|
+
let currentOffset = cfg.initialOffset ?? 0;
|
|
2058
|
+
let currentCursor = cfg.initialCursor;
|
|
2059
|
+
let currentUrl = initialUrl;
|
|
2060
|
+
let pageCount = 0;
|
|
2061
|
+
while (currentUrl && pageCount < maxPages) {
|
|
2062
|
+
pageCount++;
|
|
2063
|
+
const pageParams = {
|
|
2064
|
+
...options?.params ?? {}
|
|
2065
|
+
};
|
|
2066
|
+
if (strategy === "page") {
|
|
2067
|
+
pageParams[pageParam] = currentPage;
|
|
2068
|
+
if (pageSize !== void 0) {
|
|
2069
|
+
pageParams[pageSizeParam] = pageSize;
|
|
2070
|
+
}
|
|
2071
|
+
} else if (strategy === "offset") {
|
|
2072
|
+
pageParams[offsetParam] = currentOffset;
|
|
2073
|
+
if (pageSize !== void 0) {
|
|
2074
|
+
pageParams[pageSizeParam] = pageSize;
|
|
2075
|
+
}
|
|
2076
|
+
} else if (strategy === "cursor") {
|
|
2077
|
+
if (currentCursor !== void 0 && currentCursor !== null) {
|
|
2078
|
+
pageParams[cursorParam] = currentCursor;
|
|
2079
|
+
}
|
|
2080
|
+
if (cfg.pageSize !== void 0) {
|
|
2081
|
+
pageParams[pageSizeParam] = cfg.pageSize;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
const res = await client.get(currentUrl, {
|
|
2085
|
+
...options,
|
|
2086
|
+
params: pageParams
|
|
2087
|
+
});
|
|
2088
|
+
yield res;
|
|
2089
|
+
if (cfg.getNextUrl) {
|
|
2090
|
+
currentUrl = cfg.getNextUrl(res) ?? null;
|
|
2091
|
+
} else if (strategy === "linkHeader" || res.headers["link"]) {
|
|
2092
|
+
currentUrl = parseNextLinkHeader(res.headers["link"]);
|
|
2093
|
+
} else if (strategy === "cursor") {
|
|
2094
|
+
if (cfg.getNextCursor) {
|
|
2095
|
+
currentCursor = cfg.getNextCursor(res);
|
|
2096
|
+
} else if (res.data && typeof res.data === "object") {
|
|
2097
|
+
const obj = res.data;
|
|
2098
|
+
currentCursor = obj["nextCursor"] ?? obj["next_cursor"] ?? obj["cursor"];
|
|
2099
|
+
} else {
|
|
2100
|
+
currentCursor = null;
|
|
2101
|
+
}
|
|
2102
|
+
if (!currentCursor) {
|
|
2103
|
+
currentUrl = null;
|
|
2104
|
+
}
|
|
2105
|
+
} else if (strategy === "offset") {
|
|
2106
|
+
const pageSize2 = cfg.pageSize ?? 10;
|
|
2107
|
+
currentOffset += pageSize2;
|
|
2108
|
+
const items = cfg.getItems ? cfg.getItems(res.data) : defaultExtractItems(res.data);
|
|
2109
|
+
if (items.length === 0 || cfg.pageSize !== void 0 && items.length < cfg.pageSize) {
|
|
2110
|
+
currentUrl = null;
|
|
2111
|
+
}
|
|
2112
|
+
} else {
|
|
2113
|
+
currentPage++;
|
|
2114
|
+
const items = cfg.getItems ? cfg.getItems(res.data) : defaultExtractItems(res.data);
|
|
2115
|
+
if (items.length === 0 || cfg.pageSize !== void 0 && items.length < cfg.pageSize) {
|
|
2116
|
+
currentUrl = null;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function* itemsGenerator(url, options) {
|
|
2122
|
+
const cfg = options?.pagination;
|
|
2123
|
+
const customGetItems = cfg?.getItems;
|
|
2124
|
+
const extractFn = customGetItems ? (d) => customGetItems(d) : (d) => defaultExtractItems(d);
|
|
2125
|
+
for await (const page of pagesGenerator(url, options)) {
|
|
2126
|
+
const items = extractFn(page.data);
|
|
2127
|
+
for (const item of items) {
|
|
2128
|
+
yield item;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
async function all(url, options) {
|
|
2133
|
+
const results = [];
|
|
2134
|
+
for await (const item of itemsGenerator(url, options)) {
|
|
2135
|
+
results.push(item);
|
|
2136
|
+
}
|
|
2137
|
+
return results;
|
|
2138
|
+
}
|
|
2139
|
+
const helper = itemsGenerator;
|
|
2140
|
+
helper.pages = pagesGenerator;
|
|
2141
|
+
helper.all = all;
|
|
2142
|
+
return helper;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/client.ts
|
|
2146
|
+
function createClient(config = {}) {
|
|
2147
|
+
const clientConfig = { ...config };
|
|
2148
|
+
const authManager = new AuthManager(config.auth);
|
|
2149
|
+
const dedupManager = new DedupManager();
|
|
2150
|
+
const cacheManager = new CacheManager(config.cache);
|
|
2151
|
+
const circuitBreaker = config.circuitBreaker?.enabled ? new CircuitBreaker(config.circuitBreaker) : void 0;
|
|
2152
|
+
const concurrencyLimiter = config.maxConcurrentRequests !== void 0 && config.maxConcurrentRequests > 0 ? new ConcurrencyLimiter(config.maxConcurrentRequests) : void 0;
|
|
2153
|
+
const requestInterceptors = new DefaultInterceptorManager();
|
|
2154
|
+
const responseInterceptors = new DefaultInterceptorManager();
|
|
2155
|
+
const request = async (reqConfig) => {
|
|
2156
|
+
let currentConfig = { ...reqConfig };
|
|
2157
|
+
for (const interceptor of requestInterceptors.getInterceptors()) {
|
|
2158
|
+
try {
|
|
2159
|
+
currentConfig = await interceptor.fulfilled(currentConfig);
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
if (interceptor.rejected) {
|
|
2162
|
+
currentConfig = await interceptor.rejected(err);
|
|
2163
|
+
} else {
|
|
2164
|
+
throw err;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
let response;
|
|
2169
|
+
try {
|
|
2170
|
+
response = await executeRequest(
|
|
2171
|
+
currentConfig,
|
|
2172
|
+
clientConfig,
|
|
2173
|
+
authManager,
|
|
2174
|
+
dedupManager,
|
|
2175
|
+
cacheManager,
|
|
2176
|
+
circuitBreaker,
|
|
2177
|
+
concurrencyLimiter
|
|
2178
|
+
);
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
let currentError = err;
|
|
2181
|
+
for (const interceptor of responseInterceptors.getInterceptors()) {
|
|
2182
|
+
if (interceptor.rejected) {
|
|
2183
|
+
try {
|
|
2184
|
+
const recovered = await interceptor.rejected(currentError);
|
|
2185
|
+
return recovered;
|
|
2186
|
+
} catch (rejectedErr) {
|
|
2187
|
+
currentError = rejectedErr;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
throw currentError;
|
|
2192
|
+
}
|
|
2193
|
+
let currentResponse = response;
|
|
2194
|
+
for (const interceptor of responseInterceptors.getInterceptors()) {
|
|
2195
|
+
try {
|
|
2196
|
+
currentResponse = await interceptor.fulfilled(currentResponse);
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
if (interceptor.rejected) {
|
|
2199
|
+
currentResponse = await interceptor.rejected(err);
|
|
2200
|
+
} else {
|
|
2201
|
+
throw err;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
const shouldUnwrap = currentConfig.unwrap ?? clientConfig.unwrap ?? false;
|
|
2206
|
+
if (shouldUnwrap) {
|
|
2207
|
+
return currentResponse.data;
|
|
2208
|
+
}
|
|
2209
|
+
return currentResponse;
|
|
2210
|
+
};
|
|
2211
|
+
const safeRequest = async (reqConfig) => {
|
|
2212
|
+
try {
|
|
2213
|
+
const res = await request({ ...reqConfig, unwrap: false });
|
|
2214
|
+
return [res.data, null];
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
const fncError = isFetchNativeError(err) ? err : new NetworkError(err instanceof Error ? err.message : String(err), { cause: err });
|
|
2217
|
+
return [null, fncError];
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
const safe = {
|
|
2221
|
+
get: (url, reqConfig) => safeRequest({ ...reqConfig, url, method: "GET" }),
|
|
2222
|
+
post: (url, body, reqConfig) => {
|
|
2223
|
+
const merged = {
|
|
2224
|
+
...reqConfig,
|
|
2225
|
+
url,
|
|
2226
|
+
method: "POST"
|
|
2227
|
+
};
|
|
2228
|
+
if (body !== void 0) merged.body = body;
|
|
2229
|
+
return safeRequest(merged);
|
|
2230
|
+
},
|
|
2231
|
+
put: (url, body, reqConfig) => {
|
|
2232
|
+
const merged = {
|
|
2233
|
+
...reqConfig,
|
|
2234
|
+
url,
|
|
2235
|
+
method: "PUT"
|
|
2236
|
+
};
|
|
2237
|
+
if (body !== void 0) merged.body = body;
|
|
2238
|
+
return safeRequest(merged);
|
|
2239
|
+
},
|
|
2240
|
+
patch: (url, body, reqConfig) => {
|
|
2241
|
+
const merged = {
|
|
2242
|
+
...reqConfig,
|
|
2243
|
+
url,
|
|
2244
|
+
method: "PATCH"
|
|
2245
|
+
};
|
|
2246
|
+
if (body !== void 0) merged.body = body;
|
|
2247
|
+
return safeRequest(merged);
|
|
2248
|
+
},
|
|
2249
|
+
delete: (url, reqConfig) => safeRequest({ ...reqConfig, url, method: "DELETE" }),
|
|
2250
|
+
request: (reqConfig) => safeRequest(reqConfig)
|
|
2251
|
+
};
|
|
2252
|
+
async function* stream(url, options) {
|
|
2253
|
+
const reqConfig = {
|
|
2254
|
+
...options,
|
|
2255
|
+
url,
|
|
2256
|
+
method: options?.method ?? "GET",
|
|
2257
|
+
responseType: "stream",
|
|
2258
|
+
unwrap: false
|
|
2259
|
+
};
|
|
2260
|
+
const response = await request(reqConfig);
|
|
2261
|
+
if (!response.data || typeof response.data.getReader !== "function") {
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
yield* parseSseStream(response.data, options);
|
|
2265
|
+
}
|
|
2266
|
+
const upload = async (url, file, options) => {
|
|
2267
|
+
let formData;
|
|
2268
|
+
const fieldName = options?.fieldName ?? "file";
|
|
2269
|
+
if (typeof FormData !== "undefined" && file instanceof FormData) {
|
|
2270
|
+
formData = file;
|
|
2271
|
+
if (options?.extraFields) {
|
|
2272
|
+
for (const [k, v] of Object.entries(options.extraFields)) {
|
|
2273
|
+
formData.append(k, String(v));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
} else if (typeof FormData !== "undefined") {
|
|
2277
|
+
formData = new FormData();
|
|
2278
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
2279
|
+
if (options?.fileName) {
|
|
2280
|
+
formData.append(fieldName, file, options.fileName);
|
|
2281
|
+
} else {
|
|
2282
|
+
formData.append(fieldName, file);
|
|
2283
|
+
}
|
|
2284
|
+
} else if (file instanceof ArrayBuffer || file instanceof Uint8Array) {
|
|
2285
|
+
const buffer = file instanceof Uint8Array ? file.buffer : file;
|
|
2286
|
+
const blob = new Blob([buffer]);
|
|
2287
|
+
if (options?.fileName) {
|
|
2288
|
+
formData.append(fieldName, blob, options.fileName);
|
|
2289
|
+
} else {
|
|
2290
|
+
formData.append(fieldName, blob);
|
|
2291
|
+
}
|
|
2292
|
+
} else if (typeof file === "object" && file !== null) {
|
|
2293
|
+
for (const [k, v] of Object.entries(file)) {
|
|
2294
|
+
if (typeof Blob !== "undefined" && v instanceof Blob) {
|
|
2295
|
+
formData.append(k, v);
|
|
2296
|
+
} else if (v !== void 0 && v !== null) {
|
|
2297
|
+
formData.append(k, String(v));
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (options?.extraFields) {
|
|
2302
|
+
for (const [k, v] of Object.entries(options.extraFields)) {
|
|
2303
|
+
formData.append(k, String(v));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
} else {
|
|
2307
|
+
throw new ConfigError("FormData is not available in this environment");
|
|
2308
|
+
}
|
|
2309
|
+
const mergedConfig = {
|
|
2310
|
+
...options,
|
|
2311
|
+
url,
|
|
2312
|
+
method: options?.method ?? "POST",
|
|
2313
|
+
body: formData
|
|
2314
|
+
};
|
|
2315
|
+
return request(mergedConfig);
|
|
2316
|
+
};
|
|
2317
|
+
const client = {
|
|
2318
|
+
request,
|
|
2319
|
+
get: (url, reqConfig) => request({ ...reqConfig, url, method: "GET" }),
|
|
2320
|
+
post: (url, body, reqConfig) => {
|
|
2321
|
+
const merged = {
|
|
2322
|
+
...reqConfig,
|
|
2323
|
+
url,
|
|
2324
|
+
method: "POST"
|
|
2325
|
+
};
|
|
2326
|
+
if (body !== void 0) merged.body = body;
|
|
2327
|
+
return request(merged);
|
|
2328
|
+
},
|
|
2329
|
+
put: (url, body, reqConfig) => {
|
|
2330
|
+
const merged = {
|
|
2331
|
+
...reqConfig,
|
|
2332
|
+
url,
|
|
2333
|
+
method: "PUT"
|
|
2334
|
+
};
|
|
2335
|
+
if (body !== void 0) merged.body = body;
|
|
2336
|
+
return request(merged);
|
|
2337
|
+
},
|
|
2338
|
+
patch: (url, body, reqConfig) => {
|
|
2339
|
+
const merged = {
|
|
2340
|
+
...reqConfig,
|
|
2341
|
+
url,
|
|
2342
|
+
method: "PATCH"
|
|
2343
|
+
};
|
|
2344
|
+
if (body !== void 0) merged.body = body;
|
|
2345
|
+
return request(merged);
|
|
2346
|
+
},
|
|
2347
|
+
delete: (url, reqConfig) => request({ ...reqConfig, url, method: "DELETE" }),
|
|
2348
|
+
safe,
|
|
2349
|
+
stream,
|
|
2350
|
+
upload,
|
|
2351
|
+
paginate: null,
|
|
2352
|
+
cache: cacheManager,
|
|
2353
|
+
interceptors: {
|
|
2354
|
+
request: requestInterceptors,
|
|
2355
|
+
response: responseInterceptors
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
client.paginate = createPaginateHelper(client);
|
|
2359
|
+
return client;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/mock.ts
|
|
2363
|
+
function createMockAdapter(client, options) {
|
|
2364
|
+
const handlers = [];
|
|
2365
|
+
const history = {
|
|
2366
|
+
get: [],
|
|
2367
|
+
post: [],
|
|
2368
|
+
put: [],
|
|
2369
|
+
patch: [],
|
|
2370
|
+
delete: [],
|
|
2371
|
+
head: [],
|
|
2372
|
+
options: [],
|
|
2373
|
+
all: []
|
|
2374
|
+
};
|
|
2375
|
+
function matchesUrl(pattern, targetUrl) {
|
|
2376
|
+
if (pattern instanceof RegExp) {
|
|
2377
|
+
return pattern.test(targetUrl);
|
|
2378
|
+
}
|
|
2379
|
+
if (targetUrl === pattern) {
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
const parsed = new URL(targetUrl, "http://localhost");
|
|
2384
|
+
return parsed.pathname === pattern || parsed.pathname + parsed.search === pattern || targetUrl.includes(pattern);
|
|
2385
|
+
} catch {
|
|
2386
|
+
return targetUrl.includes(pattern);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function registerHandler(method, url = "") {
|
|
2390
|
+
const handler = {
|
|
2391
|
+
method,
|
|
2392
|
+
url,
|
|
2393
|
+
action: "reply",
|
|
2394
|
+
status: 200,
|
|
2395
|
+
reply(statusOrCallback, data, headers) {
|
|
2396
|
+
if (typeof statusOrCallback === "function") {
|
|
2397
|
+
handler.action = "reply";
|
|
2398
|
+
handler.callback = statusOrCallback;
|
|
2399
|
+
} else {
|
|
2400
|
+
handler.action = "reply";
|
|
2401
|
+
handler.status = statusOrCallback;
|
|
2402
|
+
handler.data = data;
|
|
2403
|
+
if (headers !== void 0) {
|
|
2404
|
+
handler.headers = headers;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
},
|
|
2408
|
+
replyWithTimeout() {
|
|
2409
|
+
handler.action = "timeout";
|
|
2410
|
+
},
|
|
2411
|
+
replyWithNetworkError(message) {
|
|
2412
|
+
handler.action = "networkError";
|
|
2413
|
+
handler.errorMessage = message ?? "Network request failed";
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
handlers.push(handler);
|
|
2417
|
+
return handler;
|
|
2418
|
+
}
|
|
2419
|
+
const mockFetch = async (input, init) => {
|
|
2420
|
+
if (options?.delayMs && options.delayMs > 0) {
|
|
2421
|
+
await new Promise((resolve) => setTimeout(resolve, options.delayMs));
|
|
2422
|
+
}
|
|
2423
|
+
let urlString;
|
|
2424
|
+
let method = init?.method?.toUpperCase() ?? "GET";
|
|
2425
|
+
if (typeof input === "string") {
|
|
2426
|
+
urlString = input;
|
|
2427
|
+
} else if (input instanceof URL) {
|
|
2428
|
+
urlString = input.toString();
|
|
2429
|
+
} else {
|
|
2430
|
+
urlString = input.url;
|
|
2431
|
+
method = input.method.toUpperCase() ?? method;
|
|
2432
|
+
}
|
|
2433
|
+
const historyItem = {
|
|
2434
|
+
url: urlString,
|
|
2435
|
+
method
|
|
2436
|
+
};
|
|
2437
|
+
if (init?.headers) {
|
|
2438
|
+
const hdrs = {};
|
|
2439
|
+
if (typeof Headers !== "undefined" && init.headers instanceof Headers) {
|
|
2440
|
+
init.headers.forEach((v, k) => {
|
|
2441
|
+
hdrs[k] = v;
|
|
2442
|
+
});
|
|
2443
|
+
} else if (Array.isArray(init.headers)) {
|
|
2444
|
+
for (const [k, v] of init.headers) {
|
|
2445
|
+
hdrs[k] = v;
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
Object.assign(hdrs, init.headers);
|
|
2449
|
+
}
|
|
2450
|
+
historyItem.headers = hdrs;
|
|
2451
|
+
}
|
|
2452
|
+
if (init?.body) {
|
|
2453
|
+
historyItem.body = init.body;
|
|
2454
|
+
}
|
|
2455
|
+
const lowerMethod = method.toLowerCase();
|
|
2456
|
+
if (history[lowerMethod]) {
|
|
2457
|
+
history[lowerMethod].push(historyItem);
|
|
2458
|
+
}
|
|
2459
|
+
history.all.push(historyItem);
|
|
2460
|
+
let matched;
|
|
2461
|
+
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
2462
|
+
const h = handlers[i];
|
|
2463
|
+
if (!h) continue;
|
|
2464
|
+
if (h.method === "ANY" || h.method === method) {
|
|
2465
|
+
if (!h.url || matchesUrl(h.url, urlString)) {
|
|
2466
|
+
matched = h;
|
|
2467
|
+
break;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (!matched) {
|
|
2472
|
+
if (options?.passthrough && typeof globalThis.fetch === "function") {
|
|
2473
|
+
return globalThis.fetch(input, init);
|
|
2474
|
+
}
|
|
2475
|
+
return new Response(
|
|
2476
|
+
JSON.stringify({
|
|
2477
|
+
error: `[react-native-fetch-client-mock] No mock handler registered for ${method} ${urlString}`
|
|
2478
|
+
}),
|
|
2479
|
+
{
|
|
2480
|
+
status: 404,
|
|
2481
|
+
statusText: "Not Found",
|
|
2482
|
+
headers: { "content-type": "application/json" }
|
|
2483
|
+
}
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
if (matched.action === "timeout") {
|
|
2487
|
+
throw new TimeoutError(0);
|
|
2488
|
+
}
|
|
2489
|
+
if (matched.action === "networkError") {
|
|
2490
|
+
throw new TypeError(matched.errorMessage ?? "Network request failed");
|
|
2491
|
+
}
|
|
2492
|
+
let status = matched.status ?? 200;
|
|
2493
|
+
let data = matched.data;
|
|
2494
|
+
let headers = matched.headers ?? {};
|
|
2495
|
+
if (matched.callback) {
|
|
2496
|
+
const reqConfig = {
|
|
2497
|
+
url: urlString,
|
|
2498
|
+
method
|
|
2499
|
+
};
|
|
2500
|
+
if (init?.body !== void 0) {
|
|
2501
|
+
reqConfig.body = init.body;
|
|
2502
|
+
}
|
|
2503
|
+
const cbResult = await matched.callback(reqConfig);
|
|
2504
|
+
status = cbResult[0];
|
|
2505
|
+
if (cbResult[1] !== void 0) data = cbResult[1];
|
|
2506
|
+
if (cbResult[2] !== void 0) headers = cbResult[2];
|
|
2507
|
+
}
|
|
2508
|
+
let bodyText = "";
|
|
2509
|
+
if (typeof data === "string") {
|
|
2510
|
+
bodyText = data;
|
|
2511
|
+
} else if (data !== void 0 && data !== null) {
|
|
2512
|
+
bodyText = JSON.stringify(data);
|
|
2513
|
+
}
|
|
2514
|
+
const resHeaders = {
|
|
2515
|
+
"content-type": "application/json",
|
|
2516
|
+
...headers
|
|
2517
|
+
};
|
|
2518
|
+
return new Response(bodyText, {
|
|
2519
|
+
status,
|
|
2520
|
+
statusText: status === 200 ? "OK" : "Mock Response",
|
|
2521
|
+
headers: resHeaders
|
|
2522
|
+
});
|
|
2523
|
+
};
|
|
2524
|
+
const interceptorId = client.interceptors.request.use((config) => {
|
|
2525
|
+
config.fetchImpl = mockFetch;
|
|
2526
|
+
return config;
|
|
2527
|
+
});
|
|
2528
|
+
return {
|
|
2529
|
+
onGet: (url = "") => registerHandler("GET", url),
|
|
2530
|
+
onPost: (url = "") => registerHandler("POST", url),
|
|
2531
|
+
onPut: (url = "") => registerHandler("PUT", url),
|
|
2532
|
+
onPatch: (url = "") => registerHandler("PATCH", url),
|
|
2533
|
+
onDelete: (url = "") => registerHandler("DELETE", url),
|
|
2534
|
+
onAny: (url = "") => registerHandler("ANY", url),
|
|
2535
|
+
reset() {
|
|
2536
|
+
handlers.length = 0;
|
|
2537
|
+
this.resetHistory();
|
|
2538
|
+
},
|
|
2539
|
+
resetHandlers() {
|
|
2540
|
+
handlers.length = 0;
|
|
2541
|
+
},
|
|
2542
|
+
resetHistory() {
|
|
2543
|
+
for (const key of Object.keys(history)) {
|
|
2544
|
+
history[key].length = 0;
|
|
2545
|
+
}
|
|
2546
|
+
},
|
|
2547
|
+
restore() {
|
|
2548
|
+
client.interceptors.request.eject(interceptorId);
|
|
2549
|
+
this.reset();
|
|
2550
|
+
},
|
|
2551
|
+
history
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/offline/storage.ts
|
|
2556
|
+
var MemoryStorageAdapter = class {
|
|
2557
|
+
store = /* @__PURE__ */ new Map();
|
|
2558
|
+
getItem(key) {
|
|
2559
|
+
return this.store.get(key) ?? null;
|
|
2560
|
+
}
|
|
2561
|
+
setItem(key, value) {
|
|
2562
|
+
this.store.set(key, value);
|
|
2563
|
+
}
|
|
2564
|
+
removeItem(key) {
|
|
2565
|
+
this.store.delete(key);
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// src/offline/queue.ts
|
|
2570
|
+
var DEFAULT_STORAGE_KEY = "fnc_offline_queue";
|
|
2571
|
+
var DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
2572
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
2573
|
+
var DEFAULT_MAX_AGE_MS = 864e5;
|
|
2574
|
+
var DEFAULT_IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
2575
|
+
var DEFAULT_DISALLOWED_PATTERNS = [
|
|
2576
|
+
/\/pay(\/|$)/i,
|
|
2577
|
+
/\/payment(\/|$)/i,
|
|
2578
|
+
/\/checkout(\/|$)/i,
|
|
2579
|
+
/\/orders?(\/|$)/i,
|
|
2580
|
+
/\/charge(\/|$)/i,
|
|
2581
|
+
/\/billing(\/|$)/i,
|
|
2582
|
+
/\/subscribe(\/|$)/i,
|
|
2583
|
+
/\/refund(\/|$)/i
|
|
2584
|
+
];
|
|
2585
|
+
function generateId() {
|
|
2586
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2587
|
+
return crypto.randomUUID();
|
|
2588
|
+
}
|
|
2589
|
+
return "req_" + Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
2590
|
+
}
|
|
2591
|
+
function isDisallowed(url, customPatterns) {
|
|
2592
|
+
for (const pattern of DEFAULT_DISALLOWED_PATTERNS) {
|
|
2593
|
+
if (pattern.test(url)) return true;
|
|
2594
|
+
}
|
|
2595
|
+
if (customPatterns) {
|
|
2596
|
+
for (const pattern of customPatterns) {
|
|
2597
|
+
if (typeof pattern === "string") {
|
|
2598
|
+
if (url.includes(pattern)) return true;
|
|
2599
|
+
} else if (pattern.test(url)) {
|
|
2600
|
+
return true;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
function serializeBody2(body) {
|
|
2607
|
+
if (body === void 0 || body === null) return void 0;
|
|
2608
|
+
if (typeof body === "string") return body;
|
|
2609
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
2610
|
+
throw new ConfigError("FormData bodies cannot be serialized to persistent offline storage");
|
|
2611
|
+
}
|
|
2612
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
2613
|
+
throw new ConfigError("Blob bodies cannot be serialized to persistent offline storage");
|
|
2614
|
+
}
|
|
2615
|
+
if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
|
|
2616
|
+
throw new ConfigError("ArrayBuffer bodies cannot be serialized to persistent offline storage");
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
return JSON.stringify(body);
|
|
2620
|
+
} catch {
|
|
2621
|
+
throw new ConfigError("Request body cannot be serialized for offline queue");
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function createOfflineQueue(config) {
|
|
2625
|
+
const storage = config.storage ?? new MemoryStorageAdapter();
|
|
2626
|
+
const storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
2627
|
+
const maxQueueSize = config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
2628
|
+
const maxAttempts = config.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2629
|
+
const maxAgeMs = config.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2630
|
+
const idempotencyHeader = config.idempotencyHeader ?? DEFAULT_IDEMPOTENCY_HEADER;
|
|
2631
|
+
let initialized = false;
|
|
2632
|
+
let processing = false;
|
|
2633
|
+
let inMemoryQueue = [];
|
|
2634
|
+
const persist = async () => {
|
|
2635
|
+
const raw = JSON.stringify(inMemoryQueue);
|
|
2636
|
+
await storage.setItem(storageKey, raw);
|
|
2637
|
+
};
|
|
2638
|
+
const initialize = async () => {
|
|
2639
|
+
if (initialized) return;
|
|
2640
|
+
try {
|
|
2641
|
+
const stored = await storage.getItem(storageKey);
|
|
2642
|
+
if (stored) {
|
|
2643
|
+
const parsed = JSON.parse(stored);
|
|
2644
|
+
const now = Date.now();
|
|
2645
|
+
inMemoryQueue = parsed.filter((item) => now - item.enqueuedAt < maxAgeMs);
|
|
2646
|
+
}
|
|
2647
|
+
} catch {
|
|
2648
|
+
inMemoryQueue = [];
|
|
2649
|
+
}
|
|
2650
|
+
initialized = true;
|
|
2651
|
+
await persist();
|
|
2652
|
+
};
|
|
2653
|
+
const enqueue = async (reqConfig) => {
|
|
2654
|
+
await initialize();
|
|
2655
|
+
const url = reqConfig.url ?? "";
|
|
2656
|
+
if (!url) {
|
|
2657
|
+
throw new ConfigError("Cannot enqueue request without a URL");
|
|
2658
|
+
}
|
|
2659
|
+
if (isDisallowed(url, config.disallowedUrls)) {
|
|
2660
|
+
throw new ConfigError(
|
|
2661
|
+
`Offline queuing is prohibited for critical/payment URL: "${url}". Please retry with network connectivity.`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
if (config.shouldQueue && !config.shouldQueue(reqConfig)) {
|
|
2665
|
+
return { enqueued: false, id: "" };
|
|
2666
|
+
}
|
|
2667
|
+
if (inMemoryQueue.length >= maxQueueSize) {
|
|
2668
|
+
throw new ConfigError(
|
|
2669
|
+
`Offline queue capacity limit of ${maxQueueSize} entries reached. Cannot enqueue request.`
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
const method = reqConfig.method ?? "GET";
|
|
2673
|
+
const serializedBody = serializeBody2(reqConfig.body);
|
|
2674
|
+
const id = generateId();
|
|
2675
|
+
let assignedIdempotencyKey = "";
|
|
2676
|
+
const normHeaders = {};
|
|
2677
|
+
if (reqConfig.headers) {
|
|
2678
|
+
if (typeof reqConfig.headers.forEach === "function") {
|
|
2679
|
+
reqConfig.headers.forEach((v, k) => {
|
|
2680
|
+
normHeaders[k.toLowerCase()] = v;
|
|
2681
|
+
});
|
|
2682
|
+
} else if (Array.isArray(reqConfig.headers)) {
|
|
2683
|
+
for (const [k, v] of reqConfig.headers) {
|
|
2684
|
+
normHeaders[k.toLowerCase()] = v;
|
|
2685
|
+
}
|
|
2686
|
+
} else {
|
|
2687
|
+
for (const [k, v] of Object.entries(reqConfig.headers)) {
|
|
2688
|
+
if (typeof v === "string") {
|
|
2689
|
+
normHeaders[k.toLowerCase()] = v;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
assignedIdempotencyKey = normHeaders[idempotencyHeader.toLowerCase()] ?? normHeaders["idempotency-key"] ?? normHeaders["x-idempotency-key"] ?? "";
|
|
2694
|
+
}
|
|
2695
|
+
if (!assignedIdempotencyKey) {
|
|
2696
|
+
assignedIdempotencyKey = id;
|
|
2697
|
+
normHeaders[idempotencyHeader.toLowerCase()] = assignedIdempotencyKey;
|
|
2698
|
+
}
|
|
2699
|
+
const queuedItem = {
|
|
2700
|
+
id,
|
|
2701
|
+
url,
|
|
2702
|
+
method,
|
|
2703
|
+
headers: normHeaders,
|
|
2704
|
+
enqueuedAt: Date.now(),
|
|
2705
|
+
attemptCount: 0,
|
|
2706
|
+
maxAttempts,
|
|
2707
|
+
idempotencyKey: assignedIdempotencyKey
|
|
2708
|
+
};
|
|
2709
|
+
if (reqConfig.params) {
|
|
2710
|
+
queuedItem.params = { ...reqConfig.params };
|
|
2711
|
+
}
|
|
2712
|
+
if (serializedBody !== void 0) {
|
|
2713
|
+
queuedItem.body = serializedBody;
|
|
2714
|
+
}
|
|
2715
|
+
inMemoryQueue.push(queuedItem);
|
|
2716
|
+
await persist();
|
|
2717
|
+
return { enqueued: true, id };
|
|
2718
|
+
};
|
|
2719
|
+
const process = async () => {
|
|
2720
|
+
await initialize();
|
|
2721
|
+
if (processing) {
|
|
2722
|
+
return {
|
|
2723
|
+
processed: 0,
|
|
2724
|
+
succeeded: 0,
|
|
2725
|
+
failedPermanent: 0,
|
|
2726
|
+
failedTransient: 0,
|
|
2727
|
+
remaining: inMemoryQueue.length
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
processing = true;
|
|
2731
|
+
let succeeded = 0;
|
|
2732
|
+
let failedPermanent = 0;
|
|
2733
|
+
let failedTransient = 0;
|
|
2734
|
+
let processed = 0;
|
|
2735
|
+
try {
|
|
2736
|
+
while (inMemoryQueue.length > 0) {
|
|
2737
|
+
const current = inMemoryQueue[0];
|
|
2738
|
+
if (!current) break;
|
|
2739
|
+
if (Date.now() - current.enqueuedAt > maxAgeMs) {
|
|
2740
|
+
inMemoryQueue.shift();
|
|
2741
|
+
failedPermanent++;
|
|
2742
|
+
processed++;
|
|
2743
|
+
config.onPermanentFailure?.(
|
|
2744
|
+
current,
|
|
2745
|
+
new ConfigError(`Queued request expired after ${maxAgeMs}ms`)
|
|
2746
|
+
);
|
|
2747
|
+
await persist();
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
current.attemptCount++;
|
|
2751
|
+
processed++;
|
|
2752
|
+
let parsedBody = current.body;
|
|
2753
|
+
if (current.body && (current.body.startsWith("{") || current.body.startsWith("["))) {
|
|
2754
|
+
try {
|
|
2755
|
+
parsedBody = JSON.parse(current.body);
|
|
2756
|
+
} catch {
|
|
2757
|
+
parsedBody = current.body;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
const requestConfig = {
|
|
2761
|
+
url: current.url,
|
|
2762
|
+
method: current.method
|
|
2763
|
+
};
|
|
2764
|
+
if (current.headers) {
|
|
2765
|
+
requestConfig.headers = current.headers;
|
|
2766
|
+
}
|
|
2767
|
+
if (current.params) {
|
|
2768
|
+
requestConfig.params = current.params;
|
|
2769
|
+
}
|
|
2770
|
+
if (parsedBody !== void 0 && parsedBody !== null) {
|
|
2771
|
+
requestConfig.body = parsedBody;
|
|
2772
|
+
}
|
|
2773
|
+
try {
|
|
2774
|
+
const response = await config.client.request(requestConfig);
|
|
2775
|
+
inMemoryQueue.shift();
|
|
2776
|
+
succeeded++;
|
|
2777
|
+
await persist();
|
|
2778
|
+
config.onReplaySuccess?.(current, response);
|
|
2779
|
+
} catch (err) {
|
|
2780
|
+
const is4xx = isHttpError(err) && err.status >= 400 && err.status < 500 && err.status !== 429;
|
|
2781
|
+
const isExhausted = current.attemptCount >= current.maxAttempts;
|
|
2782
|
+
if (is4xx || isExhausted) {
|
|
2783
|
+
inMemoryQueue.shift();
|
|
2784
|
+
failedPermanent++;
|
|
2785
|
+
await persist();
|
|
2786
|
+
config.onPermanentFailure?.(current, err);
|
|
2787
|
+
} else if (isNetworkError(err) || isTimeoutError(err)) {
|
|
2788
|
+
failedTransient++;
|
|
2789
|
+
await persist();
|
|
2790
|
+
break;
|
|
2791
|
+
} else {
|
|
2792
|
+
failedTransient++;
|
|
2793
|
+
await persist();
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
} finally {
|
|
2799
|
+
processing = false;
|
|
2800
|
+
}
|
|
2801
|
+
return {
|
|
2802
|
+
processed,
|
|
2803
|
+
succeeded,
|
|
2804
|
+
failedPermanent,
|
|
2805
|
+
failedTransient,
|
|
2806
|
+
remaining: inMemoryQueue.length
|
|
2807
|
+
};
|
|
2808
|
+
};
|
|
2809
|
+
const getItems = async () => {
|
|
2810
|
+
await initialize();
|
|
2811
|
+
return [...inMemoryQueue];
|
|
2812
|
+
};
|
|
2813
|
+
const size = async () => {
|
|
2814
|
+
await initialize();
|
|
2815
|
+
return inMemoryQueue.length;
|
|
2816
|
+
};
|
|
2817
|
+
const clear = async () => {
|
|
2818
|
+
inMemoryQueue = [];
|
|
2819
|
+
initialized = true;
|
|
2820
|
+
await storage.removeItem(storageKey);
|
|
2821
|
+
};
|
|
2822
|
+
let onlineListener;
|
|
2823
|
+
if (config.autoSyncOnOnline && typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
2824
|
+
onlineListener = () => {
|
|
2825
|
+
process().catch(() => {
|
|
2826
|
+
});
|
|
2827
|
+
};
|
|
2828
|
+
window.addEventListener("online", onlineListener);
|
|
2829
|
+
}
|
|
2830
|
+
const destroy = () => {
|
|
2831
|
+
if (onlineListener && typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
2832
|
+
window.removeEventListener("online", onlineListener);
|
|
2833
|
+
onlineListener = void 0;
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const isProcessing = () => processing;
|
|
2837
|
+
return {
|
|
2838
|
+
enqueue,
|
|
2839
|
+
process,
|
|
2840
|
+
isProcessing,
|
|
2841
|
+
getItems,
|
|
2842
|
+
size,
|
|
2843
|
+
clear,
|
|
2844
|
+
initialize,
|
|
2845
|
+
destroy
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
export { AuthManager, CacheManager, CancelError, CircuitBreaker, CircuitBreakerError, ConcurrencyLimiter, ConfigError, DedupManager, DefaultInterceptorManager, FetchNativeError, HttpError, MemoryStorageAdapter, NetworkError, ParseError, RetryExhaustedError, TimeoutError, ValidationError, buildUrl, cloneData, createClient, createMockAdapter, createOfflineQueue, createPaginateHelper, generateCacheKey, generateDedupKey, interpolatePath, isCacheEligible, isCancelError, isCircuitBreakerError, isFetchNativeError, isHttpError, isNetworkError, isRetryExhaustedError, isTimeoutError, isValidationError, parseNextLinkHeader, parseSseStream, validateData };
|