@alwatr/fetch 10.0.3 → 10.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/README.md +151 -222
- package/dist/cache.d.ts +15 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/dedupe.d.ts +21 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dev/main.js +3 -3
- package/dist/dev/main.js.map +10 -6
- package/dist/error.d.ts +35 -15
- package/dist/error.d.ts.map +1 -1
- package/dist/fetch.d.ts +73 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/main.d.ts +1 -93
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +10 -6
- package/dist/options.d.ts +40 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/retry.d.ts +26 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/timeout.d.ts +13 -0
- package/dist/timeout.d.ts.map +1 -0
- package/dist/type.d.ts +62 -23
- package/dist/type.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/cache.ts +146 -0
- package/src/dedupe.ts +64 -0
- package/src/error.ts +67 -15
- package/src/fetch.ts +169 -0
- package/src/main.ts +1 -188
- package/src/options.ts +191 -0
- package/src/retry.ts +106 -0
- package/src/timeout.ts +85 -0
- package/src/type.ts +82 -25
- package/dist/core.d.ts +0 -34
- package/dist/core.d.ts.map +0 -1
- package/src/core.ts +0 -351
package/src/options.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {MimeTypes, type HttpMethod} from '@alwatr/http-primer';
|
|
2
|
+
import {createLogger} from '@alwatr/logger';
|
|
3
|
+
import {getGlobalThis} from '@alwatr/global-this';
|
|
4
|
+
import {parseDuration} from '@alwatr/parse-duration';
|
|
5
|
+
|
|
6
|
+
import type {AlwatrFetchOptions_, FetchOptions, InternalFetchOptions_, QueryParams} from './type.js';
|
|
7
|
+
|
|
8
|
+
export const logger_ = createLogger('@alwatr/fetch');
|
|
9
|
+
|
|
10
|
+
export const globalThis_ = getGlobalThis();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Immutable default options for all fetch requests.
|
|
14
|
+
*/
|
|
15
|
+
export const defaultFetchOptions: Readonly<AlwatrFetchOptions_> = {
|
|
16
|
+
method: 'GET',
|
|
17
|
+
timeout: 8_000,
|
|
18
|
+
retry: 3,
|
|
19
|
+
retryDelay: 1_000,
|
|
20
|
+
removeDuplicate: 'never',
|
|
21
|
+
cacheStrategy: 'network_only',
|
|
22
|
+
cacheStorageName: 'fetch_cache',
|
|
23
|
+
// headers: {}, // --- IGNORED ---
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes any standard `HeadersInit` into a fresh, isolated lowercase string record.
|
|
28
|
+
*
|
|
29
|
+
* @param headers - User-provided headers (plain object, Headers instance, or entries array).
|
|
30
|
+
* @param baseHeaders - Optional base headers to merge with the user-provided headers.
|
|
31
|
+
* @returns An isolated `Record<string, string>`.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeHeaders_(
|
|
34
|
+
headers?: HeadersInit,
|
|
35
|
+
baseHeaders: Record<string, string> = {},
|
|
36
|
+
): Record<string, string> {
|
|
37
|
+
if (headers == null) {
|
|
38
|
+
return baseHeaders;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
42
|
+
headers.forEach((value, key) => {
|
|
43
|
+
baseHeaders[key.toLowerCase()] = value;
|
|
44
|
+
});
|
|
45
|
+
return baseHeaders;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(headers)) {
|
|
49
|
+
for (const [key, value] of headers) {
|
|
50
|
+
if (typeof key === 'string' && typeof value === 'string') {
|
|
51
|
+
baseHeaders[key.toLowerCase()] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return baseHeaders;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof headers === 'object') {
|
|
58
|
+
for (const key of Object.keys(headers)) {
|
|
59
|
+
const val = (headers as Record<string, unknown>)[key];
|
|
60
|
+
if (val != null) {
|
|
61
|
+
baseHeaders[key.toLowerCase()] = String(val);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return baseHeaders;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Serializes query parameters into a query string.
|
|
71
|
+
*
|
|
72
|
+
* @param queryParams - Dictionary of query parameters.
|
|
73
|
+
* @returns Serialized URL query string (without leading `?` or `&`).
|
|
74
|
+
*/
|
|
75
|
+
export function serializeQueryParams_(queryParams: QueryParams): string {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const key of Object.keys(queryParams)) {
|
|
79
|
+
const value = queryParams[key];
|
|
80
|
+
if (value === undefined || value === null) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
if (item !== undefined && item !== null) {
|
|
87
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return parts.join('&');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Appends query parameters to a URL, correctly respecting existing query parameters and hash anchors.
|
|
100
|
+
*
|
|
101
|
+
* @param url - The target URL.
|
|
102
|
+
* @param queryParams - Query parameters to append.
|
|
103
|
+
* @returns The resulting URL string.
|
|
104
|
+
*/
|
|
105
|
+
export function appendQueryParams_(url: string, queryParams?: QueryParams): string {
|
|
106
|
+
if (queryParams == null) {
|
|
107
|
+
return url;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const queryString = serializeQueryParams_(queryParams);
|
|
111
|
+
if (queryString.length === 0) {
|
|
112
|
+
return url;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle hash fragment if present in URL
|
|
116
|
+
let baseUrl = url;
|
|
117
|
+
let hashPart = '';
|
|
118
|
+
const hashIndex = url.indexOf('#');
|
|
119
|
+
|
|
120
|
+
if (hashIndex !== -1) {
|
|
121
|
+
baseUrl = url.slice(0, hashIndex);
|
|
122
|
+
hashPart = url.slice(hashIndex);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const separator = baseUrl.includes('?') ? '&' : '?';
|
|
126
|
+
return `${baseUrl}${separator}${queryString}${hashPart}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Processes, sanitizes, and normalizes user-provided fetch options into a complete, isolated options object.
|
|
131
|
+
*
|
|
132
|
+
* @param url - The target URL.
|
|
133
|
+
* @param options - User-provided options.
|
|
134
|
+
* @returns Internal, complete, and isolated fetch options.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export function processOptions_(url: string, options: FetchOptions = {}): InternalFetchOptions_ {
|
|
138
|
+
DEV_MODE && logger_.logMethod?.('processOptions_');
|
|
139
|
+
|
|
140
|
+
const processedUrl = appendQueryParams_(url, options.queryParams);
|
|
141
|
+
|
|
142
|
+
const options_: InternalFetchOptions_ = {
|
|
143
|
+
...defaultFetchOptions,
|
|
144
|
+
...options,
|
|
145
|
+
headers: normalizeHeaders_(options.headers),
|
|
146
|
+
url: processedUrl,
|
|
147
|
+
method: (options.method?.toUpperCase() as HttpMethod) ?? defaultFetchOptions.method,
|
|
148
|
+
timeout: parseDuration(options.timeout ?? defaultFetchOptions.timeout),
|
|
149
|
+
retryDelay: parseDuration(options.retryDelay ?? defaultFetchOptions.retryDelay),
|
|
150
|
+
retry:
|
|
151
|
+
typeof options.retry === 'number' && Number.isFinite(options.retry) ?
|
|
152
|
+
Math.max(1, Math.floor(options.retry))
|
|
153
|
+
: defaultFetchOptions.retry,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
options_.window ??= null;
|
|
157
|
+
|
|
158
|
+
// Cache API Preconditions: requires Cache API runtime support and cacheable HTTP method (GET/HEAD)
|
|
159
|
+
if (
|
|
160
|
+
options_.cacheStrategy !== 'network_only'
|
|
161
|
+
&& (typeof caches === 'undefined' || (options_.method !== 'GET' && options_.method !== 'HEAD'))
|
|
162
|
+
) {
|
|
163
|
+
DEV_MODE
|
|
164
|
+
&& logger_.incident?.('processOptions_', 'fetch_cache_strategy_unsupported', {
|
|
165
|
+
method: options_.method,
|
|
166
|
+
cacheStrategy: options_.cacheStrategy,
|
|
167
|
+
hasCaches: typeof caches !== 'undefined',
|
|
168
|
+
});
|
|
169
|
+
options_.cacheStrategy = 'network_only';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Deduplication auto selection
|
|
173
|
+
if (options_.removeDuplicate === 'auto') {
|
|
174
|
+
options_.removeDuplicate = typeof caches !== 'undefined' ? 'until_load' : 'always';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// JSON Body serialization
|
|
178
|
+
if (options.bodyJson !== undefined) {
|
|
179
|
+
options_.body = JSON.stringify(options.bodyJson);
|
|
180
|
+
options_.headers['content-type'] = MimeTypes.JSON;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Authorization header configuration
|
|
184
|
+
if (options.bearerToken !== undefined) {
|
|
185
|
+
options_.headers.authorization = `Bearer ${options.bearerToken}`;
|
|
186
|
+
} else if (options.alwatrAuth !== undefined) {
|
|
187
|
+
options_.headers.authorization = `Alwatr ${options.alwatrAuth.userId}:${options.alwatrAuth.userToken}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return options_;
|
|
191
|
+
}
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {delay} from '@alwatr/delay';
|
|
2
|
+
import {getGlobalThis} from '@alwatr/global-this';
|
|
3
|
+
import {HttpStatusCodes} from '@alwatr/http-primer';
|
|
4
|
+
import {FetchError} from './error.js';
|
|
5
|
+
import {logger_} from './options.js';
|
|
6
|
+
import {handleTimeout_} from './timeout.js';
|
|
7
|
+
|
|
8
|
+
import type {InternalFetchOptions_} from './type.js';
|
|
9
|
+
|
|
10
|
+
const globalThis_ = getGlobalThis();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks whether an HTTP response status code is retryable.
|
|
14
|
+
*
|
|
15
|
+
* Retryable statuses:
|
|
16
|
+
* - Any 5xx Server Error (500, 502, 503, 504, ...)
|
|
17
|
+
* - 408 Request Timeout
|
|
18
|
+
* - 429 Too Many Requests
|
|
19
|
+
*/
|
|
20
|
+
export function isRetryableStatus_(status: number): boolean {
|
|
21
|
+
return (
|
|
22
|
+
status >= HttpStatusCodes.Error_Server_500_Internal_Server_Error
|
|
23
|
+
|| status === HttpStatusCodes.Error_Client_408_Request_Timeout
|
|
24
|
+
|| status === HttpStatusCodes.Error_Client_429_Too_Many_Requests
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parses the `Retry-After` header value (in seconds or HTTP-date) if present.
|
|
30
|
+
*
|
|
31
|
+
* @param response - The HTTP Response object.
|
|
32
|
+
* @returns Delay duration in milliseconds, or undefined if absent/invalid.
|
|
33
|
+
*/
|
|
34
|
+
export function parseRetryAfterHeader_(response?: Response): number | undefined {
|
|
35
|
+
const retryAfter = response?.headers?.get('retry-after');
|
|
36
|
+
if (!retryAfter) return undefined;
|
|
37
|
+
|
|
38
|
+
const seconds = Number(retryAfter);
|
|
39
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
40
|
+
return seconds * 1000;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dateMs = Date.parse(retryAfter);
|
|
44
|
+
if (!isNaN(dateMs)) {
|
|
45
|
+
const diff = dateMs - Date.now();
|
|
46
|
+
return diff > 0 ? diff : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Executes a fetch request with automatic retries on transient errors (5xx, 429, 408, network failures, timeouts).
|
|
54
|
+
*
|
|
55
|
+
* @param options - Processed internal fetch options.
|
|
56
|
+
* @returns A promise resolving to the final `Response` after retry cycles.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export async function handleRetryPattern_(options: InternalFetchOptions_): Promise<Response> {
|
|
60
|
+
if (options.retry <= 1) {
|
|
61
|
+
return handleTimeout_(options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
DEV_MODE && logger_.logMethod?.('handleRetryPattern_');
|
|
65
|
+
options.retry--;
|
|
66
|
+
|
|
67
|
+
let response: Response;
|
|
68
|
+
try {
|
|
69
|
+
response = await handleTimeout_(options);
|
|
70
|
+
|
|
71
|
+
if (response.ok || !isRetryableStatus_(response.status)) {
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
DEV_MODE && logger_.accident('fetch', 'fetch_failed_retry', err);
|
|
76
|
+
|
|
77
|
+
// Never retry if the request was intentionally aborted
|
|
78
|
+
if (options.signal?.aborted || (err instanceof FetchError && err.reason === 'aborted')) {
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Do not retry if the runtime is offline
|
|
83
|
+
if (globalThis_.navigator?.onLine === false) {
|
|
84
|
+
DEV_MODE && logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await delay.by(options.retryDelay);
|
|
89
|
+
|
|
90
|
+
return handleRetryPattern_(options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle transient retryable HTTP status (5xx, 429, 408)
|
|
94
|
+
DEV_MODE && logger_.accident('fetch', 'fetch_failed_retry', {status: response.status});
|
|
95
|
+
|
|
96
|
+
if (globalThis_.navigator?.onLine === false) {
|
|
97
|
+
DEV_MODE && logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const retryDelay = parseRetryAfterHeader_(response) ?? options.retryDelay;
|
|
102
|
+
|
|
103
|
+
await delay.by(retryDelay);
|
|
104
|
+
|
|
105
|
+
return handleRetryPattern_(options);
|
|
106
|
+
}
|
package/src/timeout.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {getGlobalThis} from '@alwatr/global-this';
|
|
2
|
+
import {FetchError} from './error.js';
|
|
3
|
+
import {logger_} from './options.js';
|
|
4
|
+
|
|
5
|
+
import type {InternalFetchOptions_} from './type.js';
|
|
6
|
+
|
|
7
|
+
const globalThis_ = getGlobalThis();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Executes a native `fetch` wrapped with an `AbortController` timeout.
|
|
11
|
+
*
|
|
12
|
+
* Checks for pre-aborted external signals, respects external cancellation,
|
|
13
|
+
* and guarantees listener and timer cleanup on completion.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Processed internal fetch options.
|
|
16
|
+
* @returns A promise resolving to the native `Response` or rejecting with `FetchError`.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function handleTimeout_(options: InternalFetchOptions_): Promise<Response> {
|
|
20
|
+
const externalSignal = options.signal;
|
|
21
|
+
|
|
22
|
+
// Immediate abort check: If signal is already aborted, reject immediately without network overhead
|
|
23
|
+
if (externalSignal?.aborted) {
|
|
24
|
+
DEV_MODE && logger_.incident?.('handleTimeout_', 'already_aborted', {reason: externalSignal.reason});
|
|
25
|
+
return Promise.reject(new FetchError('aborted', 'The operation was aborted'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If timeout is disabled (0), invoke native fetch directly with external signal
|
|
29
|
+
if (options.timeout === 0) {
|
|
30
|
+
return globalThis_.fetch(options.url, options as RequestInit);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DEV_MODE && logger_.logMethod?.('handleTimeout_');
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const abortController = typeof AbortController === 'function' ? new AbortController() : null;
|
|
37
|
+
|
|
38
|
+
let onExternalAbort: (() => void) | undefined;
|
|
39
|
+
|
|
40
|
+
if (abortController !== null) {
|
|
41
|
+
options.signal = abortController.signal;
|
|
42
|
+
|
|
43
|
+
if (externalSignal != null) {
|
|
44
|
+
onExternalAbort = () => {
|
|
45
|
+
abortController.abort(externalSignal.reason);
|
|
46
|
+
};
|
|
47
|
+
externalSignal.addEventListener('abort', onExternalAbort, {once: true});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let timeoutFired = false;
|
|
52
|
+
|
|
53
|
+
const timeoutId = setTimeout(() => {
|
|
54
|
+
timeoutFired = true;
|
|
55
|
+
abortController?.abort('fetch_timeout');
|
|
56
|
+
reject(new FetchError('timeout', 'fetch_timeout'));
|
|
57
|
+
}, options.timeout);
|
|
58
|
+
|
|
59
|
+
globalThis_
|
|
60
|
+
.fetch(options.url, options as RequestInit)
|
|
61
|
+
.then((response) => {
|
|
62
|
+
if (!timeoutFired) {
|
|
63
|
+
resolve(response);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch((err: unknown) => {
|
|
67
|
+
if (timeoutFired) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (externalSignal?.aborted || (err instanceof Error && err.name === 'AbortError')) {
|
|
72
|
+
reject(new FetchError('aborted', 'The operation was aborted'));
|
|
73
|
+
} else {
|
|
74
|
+
reject(err);
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
options.signal = externalSignal;
|
|
80
|
+
if (externalSignal != null && onExternalAbort !== undefined) {
|
|
81
|
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/type.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type {DictionaryOpt,
|
|
1
|
+
import type {DictionaryOpt, JsonValue} from '@alwatr/type-helper';
|
|
2
2
|
import type {FetchError} from './error.js';
|
|
3
3
|
import type {HttpMethod, HttpRequestHeaders} from '@alwatr/http-primer';
|
|
4
4
|
import type {Duration} from '@alwatr/parse-duration';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A dictionary of query parameters.
|
|
8
|
-
* Keys are strings, and values can be strings, numbers, or
|
|
8
|
+
* Keys are strings, and values can be strings, numbers, booleans, or arrays of these primitives.
|
|
9
9
|
*/
|
|
10
|
-
export type QueryParams = DictionaryOpt<
|
|
10
|
+
export type QueryParams = DictionaryOpt<
|
|
11
|
+
string | number | boolean | readonly (string | number | boolean)[] | (string | number | boolean)[]
|
|
12
|
+
>;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Defines the caching strategy for a fetch request.
|
|
@@ -47,9 +49,9 @@ export interface AlwatrFetchOptions_ {
|
|
|
47
49
|
method: HttpMethod;
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
52
|
+
* Request headers. Supports plain object, Web Standard `Headers`, or entries array.
|
|
51
53
|
*/
|
|
52
|
-
headers
|
|
54
|
+
headers?: HttpRequestHeaders | HeadersInit;
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Request timeout duration. Can be a number (milliseconds) or a string (e.g., '5s').
|
|
@@ -60,33 +62,33 @@ export interface AlwatrFetchOptions_ {
|
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Number of times to retry a failed request.
|
|
63
|
-
* Retries occur on network errors, timeouts, or 5xx server responses.
|
|
65
|
+
* Retries occur on network errors, timeouts, 408/429 status codes, or 5xx server responses.
|
|
64
66
|
* @default 3
|
|
65
67
|
*/
|
|
66
68
|
retry: number;
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Delay before each retry attempt. Can be a number (milliseconds) or a string (e.g., '
|
|
71
|
+
* Delay before each retry attempt. Can be a number (milliseconds) or a string (e.g., '1s').
|
|
70
72
|
* @default '1s'
|
|
71
73
|
*/
|
|
72
74
|
retryDelay: Duration;
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Strategy for handling duplicate parallel requests.
|
|
76
|
-
* Uniqueness is determined by method, URL,
|
|
78
|
+
* Uniqueness is determined by method, URL, query parameters, request body, and authorization context.
|
|
77
79
|
* @default 'never'
|
|
78
80
|
*/
|
|
79
81
|
removeDuplicate: CacheDuplicate;
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* The caching strategy to use for the request.
|
|
83
|
-
* Requires a browser environment with Cache API support.
|
|
85
|
+
* Requires a browser or environment with Cache API support.
|
|
84
86
|
* @default 'network_only'
|
|
85
87
|
*/
|
|
86
88
|
cacheStrategy: CacheStrategy;
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
|
-
* A callback function
|
|
91
|
+
* A callback function executed with the fresh response when using 'stale_while_revalidate'.
|
|
90
92
|
*/
|
|
91
93
|
revalidateCallback?: (response: Response) => void | Promise<void>;
|
|
92
94
|
|
|
@@ -97,7 +99,7 @@ export interface AlwatrFetchOptions_ {
|
|
|
97
99
|
cacheStorageName: string;
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
|
-
* A JavaScript
|
|
102
|
+
* A JavaScript value to be serialized as the request's JSON body.
|
|
101
103
|
* Automatically sets the 'Content-Type' header to 'application/json'.
|
|
102
104
|
*/
|
|
103
105
|
bodyJson?: JsonValue;
|
|
@@ -126,31 +128,86 @@ export interface AlwatrFetchOptions_ {
|
|
|
126
128
|
*/
|
|
127
129
|
export type FetchOptions = Partial<AlwatrFetchOptions_> & Omit<RequestInit, 'headers'>;
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Options for `fetchJson`, extending `FetchOptions` with JSON-specific flags.
|
|
133
|
+
*/
|
|
134
|
+
export type FetchJsonOptions = FetchOptions & {
|
|
135
|
+
/**
|
|
136
|
+
* If `true`, requires the parsed JSON body to have an `ok: true` property.
|
|
137
|
+
* If `ok` is missing or not `true`, fails with `json_response_error`.
|
|
138
|
+
*/
|
|
139
|
+
requireJsonResponseWithOkTrue?: true;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Represents the tuple returned by the `fetch` function.
|
|
144
|
+
* On success: `[Response, null]`. On failure: `[null, FetchError]`.
|
|
145
|
+
*/
|
|
146
|
+
export type FetchResponse = readonly [Response, null] | readonly [null, FetchError];
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
|
-
* Represents the tuple returned by
|
|
133
|
-
* On success
|
|
149
|
+
* Represents the tuple returned by `fetchJson`.
|
|
150
|
+
* On success: `[T, null]`. On failure: `[null, FetchError]`.
|
|
134
151
|
*/
|
|
135
|
-
export type
|
|
152
|
+
export type FetchJsonResponse<T = unknown> = readonly [T, null] | readonly [null, FetchError];
|
|
136
153
|
|
|
137
154
|
/**
|
|
138
155
|
* Defines the specific reason for a fetch failure.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* - `
|
|
142
|
-
* - `
|
|
143
|
-
* - `
|
|
144
|
-
* - `
|
|
145
|
-
* - `
|
|
146
|
-
* - `
|
|
156
|
+
*
|
|
157
|
+
* Semantic HTTP Client Errors (4xx):
|
|
158
|
+
* - `bad_request`: 400 Bad Request
|
|
159
|
+
* - `unauthorized`: 401 Unauthorized
|
|
160
|
+
* - `forbidden`: 403 Forbidden
|
|
161
|
+
* - `not_found`: 404 Not Found
|
|
162
|
+
* - `request_timeout`: 408 Request Timeout
|
|
163
|
+
* - `conflict`: 409 Conflict
|
|
164
|
+
* - `payload_too_large`: 413 Payload Too Large
|
|
165
|
+
* - `unprocessable_content`: 422 Unprocessable Entity / Content
|
|
166
|
+
* - `rate_limited`: 429 Too Many Requests
|
|
167
|
+
* - `http_error`: Other 4xx client errors
|
|
168
|
+
*
|
|
169
|
+
* Semantic HTTP Server Errors (5xx):
|
|
170
|
+
* - `server_error`: Any 5xx server-side error (500, 502, 503, 504, etc.)
|
|
171
|
+
*
|
|
172
|
+
* Network & Lifecycle Errors:
|
|
173
|
+
* - `timeout`: The request exceeded the configured timeout duration.
|
|
174
|
+
* - `aborted`: The request was cancelled by an AbortSignal.
|
|
175
|
+
* - `network_error`: A network-level failure occurred (DNS, connection reset, offline).
|
|
176
|
+
* - `cache_not_found`: Resource was not found when using `cache_only`.
|
|
177
|
+
* - `json_parse_error`: Response body could not be parsed as valid JSON.
|
|
178
|
+
* - `json_response_error`: Response JSON `ok` property was not true when `requireJsonResponseWithOkTrue` was set.
|
|
179
|
+
* - `unknown_error`: An unexpected or untyped error occurred.
|
|
147
180
|
*/
|
|
148
181
|
export type FetchErrorReason =
|
|
182
|
+
| 'bad_request'
|
|
183
|
+
| 'unauthorized'
|
|
184
|
+
| 'forbidden'
|
|
185
|
+
| 'not_found'
|
|
186
|
+
| 'request_timeout'
|
|
187
|
+
| 'conflict'
|
|
188
|
+
| 'payload_too_large'
|
|
189
|
+
| 'unprocessable_content'
|
|
190
|
+
| 'rate_limited'
|
|
149
191
|
| 'http_error'
|
|
150
|
-
| '
|
|
192
|
+
| 'server_error'
|
|
151
193
|
| 'timeout'
|
|
152
|
-
| 'network_error'
|
|
153
194
|
| 'aborted'
|
|
195
|
+
| 'network_error'
|
|
196
|
+
| 'cache_not_found'
|
|
154
197
|
| 'json_parse_error'
|
|
155
198
|
| 'json_response_error'
|
|
156
199
|
| 'unknown_error';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Internal-only normalized fetch options type.
|
|
203
|
+
* @internal
|
|
204
|
+
*/
|
|
205
|
+
export interface InternalFetchOptions_
|
|
206
|
+
extends Omit<AlwatrFetchOptions_, 'headers' | 'method' | 'timeout' | 'retryDelay'>,
|
|
207
|
+
Omit<RequestInit, 'headers' | 'method'> {
|
|
208
|
+
url: string;
|
|
209
|
+
method: HttpMethod;
|
|
210
|
+
headers: HttpRequestHeaders;
|
|
211
|
+
timeout: number;
|
|
212
|
+
retryDelay: number;
|
|
213
|
+
}
|
package/dist/core.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { AlwatrFetchOptions_, FetchOptions } from './type.js';
|
|
2
|
-
export declare const logger_: import("@alwatr/logger").AlwatrLogger;
|
|
3
|
-
/**
|
|
4
|
-
* A boolean flag indicating whether the browser's Cache API is supported.
|
|
5
|
-
*/
|
|
6
|
-
export declare const cacheSupported: boolean;
|
|
7
|
-
/**
|
|
8
|
-
* Internal-only fetch options type, which includes the URL and ensures all
|
|
9
|
-
* optional properties from AlwatrFetchOptions_ are present.
|
|
10
|
-
*/
|
|
11
|
-
type FetchOptions__ = AlwatrFetchOptions_ & Omit<RequestInit, 'headers'> & {
|
|
12
|
-
url: string;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Processes and sanitizes the fetch options.
|
|
16
|
-
*
|
|
17
|
-
* @param {string} url - The URL to fetch.
|
|
18
|
-
* @param {FetchOptions} options - The user-provided options.
|
|
19
|
-
* @returns {FetchOptions__} The processed and complete fetch options.
|
|
20
|
-
* @private
|
|
21
|
-
*/
|
|
22
|
-
export declare function _processOptions(url: string, options: FetchOptions): FetchOptions__;
|
|
23
|
-
/**
|
|
24
|
-
* Manages caching strategies for the fetch request.
|
|
25
|
-
* If the strategy is `network_only`, it bypasses caching and proceeds to the next step.
|
|
26
|
-
* Otherwise, it interacts with the browser's Cache API based on the selected strategy.
|
|
27
|
-
*
|
|
28
|
-
* @param {FetchOptions__} options - The fully configured fetch options.
|
|
29
|
-
* @returns {Promise<Response>} A promise resolving to a `Response` object, either from the cache or the network.
|
|
30
|
-
* @private
|
|
31
|
-
*/
|
|
32
|
-
export declare function handleCacheStrategy_(options: FetchOptions__): Promise<Response>;
|
|
33
|
-
export {};
|
|
34
|
-
//# sourceMappingURL=core.d.ts.map
|
package/dist/core.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,mBAAmB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAEjE,eAAO,MAAM,OAAO,uCAAgC,CAAC;AAIrD;;GAEG;AACH,eAAO,MAAM,cAAc,SAAgD,CAAC;AAwB5E;;;GAGG;AACH,KAAK,cAAc,GAAG,mBAAmB,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAC;AAEzF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,cAAc,CAoDlF;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6FrF"}
|