fetchguard 1.6.3 → 2.0.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 +235 -65
- package/dist/index.d.ts +540 -31
- package/dist/index.js +480 -86
- package/dist/index.js.map +1 -1
- package/dist/worker.js +140 -74
- package/dist/worker.js.map +1 -1
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as ts_micro_result from 'ts-micro-result';
|
|
2
2
|
import { Result, ErrorDetail, ResultMeta } from 'ts-micro-result';
|
|
3
|
-
import * as ts_micro_result_factories_errors_advanced from 'ts-micro-result/factories/errors-advanced';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* FetchGuard Business Types
|
|
@@ -76,6 +75,16 @@ interface TokenProvider {
|
|
|
76
75
|
*/
|
|
77
76
|
[key: string]: (...args: any[]) => Promise<Result<TokenInfo>>;
|
|
78
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Storage error context for debugging
|
|
80
|
+
*/
|
|
81
|
+
type StorageErrorContext = 'get' | 'set' | 'delete' | 'open';
|
|
82
|
+
/**
|
|
83
|
+
* Storage error callback type
|
|
84
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
85
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
86
|
+
*/
|
|
87
|
+
type StorageErrorCallback = (error: Error, context: StorageErrorContext) => void;
|
|
79
88
|
/**
|
|
80
89
|
* Interface for refresh token storage - only stores refresh token
|
|
81
90
|
*
|
|
@@ -144,6 +153,46 @@ interface FetchGuardOptions {
|
|
|
144
153
|
refreshEarlyMs?: number;
|
|
145
154
|
/** Default headers to include in all requests */
|
|
146
155
|
defaultHeaders?: Record<string, string>;
|
|
156
|
+
/**
|
|
157
|
+
* Maximum concurrent requests to worker (default: 6)
|
|
158
|
+
* Controls how many requests can be in-flight simultaneously.
|
|
159
|
+
* Set to 1 for strictly sequential processing.
|
|
160
|
+
* Higher values increase throughput but may cause worker congestion.
|
|
161
|
+
*/
|
|
162
|
+
maxConcurrent?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Maximum queue size for pending requests (default: 1000)
|
|
165
|
+
* When queue is full, new requests will immediately fail with QUEUE_FULL error.
|
|
166
|
+
* Prevents memory leak if worker is unresponsive.
|
|
167
|
+
*/
|
|
168
|
+
maxQueueSize?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Worker setup timeout in milliseconds (default: 10000)
|
|
171
|
+
* How long to wait for worker to be ready before failing.
|
|
172
|
+
*/
|
|
173
|
+
setupTimeout?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Default request timeout in milliseconds (default: 30000)
|
|
176
|
+
* How long to wait for a request to complete before timing out.
|
|
177
|
+
* Can be overridden per-request via fetch options.
|
|
178
|
+
*/
|
|
179
|
+
requestTimeout?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Debug hooks for observing operations (logging, monitoring)
|
|
182
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
183
|
+
*/
|
|
184
|
+
debug?: DebugHooks;
|
|
185
|
+
/**
|
|
186
|
+
* Retry configuration for network errors
|
|
187
|
+
* Only retries on transport failures, NOT on HTTP errors (4xx/5xx)
|
|
188
|
+
*/
|
|
189
|
+
retry?: RetryConfig;
|
|
190
|
+
/**
|
|
191
|
+
* Request deduplication configuration
|
|
192
|
+
* When enabled, duplicate requests to the same URL within a time window
|
|
193
|
+
* will share the same response instead of making multiple requests.
|
|
194
|
+
*/
|
|
195
|
+
dedupe?: DedupeConfig;
|
|
147
196
|
}
|
|
148
197
|
/**
|
|
149
198
|
* Internal worker configuration
|
|
@@ -163,25 +212,31 @@ interface FetchGuardRequestInit extends RequestInit {
|
|
|
163
212
|
includeHeaders?: boolean;
|
|
164
213
|
}
|
|
165
214
|
/**
|
|
166
|
-
*
|
|
215
|
+
* Fetch envelope - raw HTTP response from worker
|
|
216
|
+
*
|
|
217
|
+
* Worker only fetches and returns raw data, does NOT judge HTTP status.
|
|
218
|
+
* Client receives envelope and decides ok/err based on business logic.
|
|
219
|
+
*
|
|
220
|
+
* - status: HTTP status code (2xx, 3xx, 4xx, 5xx)
|
|
167
221
|
* - body: string (text/JSON) or base64 (binary)
|
|
168
222
|
* - contentType: always present, indicates how to decode body
|
|
169
223
|
* - headers: empty object if includeHeaders: false
|
|
170
|
-
* - status: HTTP status code
|
|
171
224
|
*/
|
|
172
|
-
interface
|
|
173
|
-
body: string;
|
|
225
|
+
interface FetchEnvelope {
|
|
174
226
|
status: number;
|
|
227
|
+
body: string;
|
|
175
228
|
contentType: string;
|
|
176
229
|
headers: Record<string, string>;
|
|
177
230
|
}
|
|
178
231
|
/**
|
|
179
232
|
* Serialized file data for transfer over postMessage
|
|
233
|
+
* Uses ArrayBuffer for zero-copy transfer via Transferable
|
|
180
234
|
*/
|
|
181
235
|
interface SerializedFile {
|
|
182
236
|
name: string;
|
|
183
237
|
type: string;
|
|
184
|
-
|
|
238
|
+
/** ArrayBuffer - transferred via postMessage Transferable for zero-copy */
|
|
239
|
+
buffer: ArrayBuffer;
|
|
185
240
|
}
|
|
186
241
|
/**
|
|
187
242
|
* Serialized FormData entry - can be string or file
|
|
@@ -194,6 +249,187 @@ interface SerializedFormData {
|
|
|
194
249
|
_type: 'FormData';
|
|
195
250
|
entries: Array<[string, SerializedFormDataEntry]>;
|
|
196
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Result of FormData serialization with transferables
|
|
254
|
+
* Used for zero-copy transfer via postMessage
|
|
255
|
+
*/
|
|
256
|
+
interface SerializedFormDataResult {
|
|
257
|
+
data: SerializedFormData;
|
|
258
|
+
/** ArrayBuffers to transfer - pass to postMessage as second argument */
|
|
259
|
+
transferables: ArrayBuffer[];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Network error detail for transport failures
|
|
263
|
+
* Used when no HTTP response is received (connection failed, timeout, cancelled)
|
|
264
|
+
*/
|
|
265
|
+
interface NetworkErrorDetail {
|
|
266
|
+
code: 'NETWORK_ERROR' | 'REQUEST_CANCELLED' | 'RESPONSE_PARSE_FAILED';
|
|
267
|
+
message: string;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Reason for token refresh
|
|
271
|
+
*/
|
|
272
|
+
type RefreshReason = 'expired' | 'proactive' | 'manual';
|
|
273
|
+
/**
|
|
274
|
+
* Request timing metrics for performance monitoring
|
|
275
|
+
*
|
|
276
|
+
* All times are in milliseconds.
|
|
277
|
+
*/
|
|
278
|
+
interface RequestMetrics {
|
|
279
|
+
/** When request was initiated (Date.now()) */
|
|
280
|
+
startTime: number;
|
|
281
|
+
/** When response was received (Date.now()) */
|
|
282
|
+
endTime: number;
|
|
283
|
+
/** Total duration (endTime - startTime) */
|
|
284
|
+
duration: number;
|
|
285
|
+
/** Time spent waiting in queue before processing */
|
|
286
|
+
queueTime: number;
|
|
287
|
+
/** Time spent in IPC (postMessage round-trip overhead) */
|
|
288
|
+
ipcTime: number;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Debug hooks for observing FetchGuard operations
|
|
292
|
+
*
|
|
293
|
+
* All hooks are observe-only - they cannot modify requests/responses.
|
|
294
|
+
* Useful for logging, debugging, and monitoring.
|
|
295
|
+
*
|
|
296
|
+
* Note: Hooks run synchronously and should not perform heavy operations.
|
|
297
|
+
*/
|
|
298
|
+
interface DebugHooks {
|
|
299
|
+
/**
|
|
300
|
+
* Called before each request is sent to worker
|
|
301
|
+
* @param url - Request URL
|
|
302
|
+
* @param options - Request options (method, headers, etc.)
|
|
303
|
+
*/
|
|
304
|
+
onRequest?: (url: string, options: FetchGuardRequestInit) => void;
|
|
305
|
+
/**
|
|
306
|
+
* Called when response is received from worker
|
|
307
|
+
* @param url - Request URL
|
|
308
|
+
* @param envelope - Response envelope (status, body, headers)
|
|
309
|
+
* @param metrics - Request timing metrics (optional, for performance monitoring)
|
|
310
|
+
*/
|
|
311
|
+
onResponse?: (url: string, envelope: FetchEnvelope, metrics?: RequestMetrics) => void;
|
|
312
|
+
/**
|
|
313
|
+
* Called when token refresh occurs
|
|
314
|
+
* @param reason - Why refresh happened: 'expired', 'proactive', or 'manual'
|
|
315
|
+
*/
|
|
316
|
+
onRefresh?: (reason: RefreshReason) => void;
|
|
317
|
+
/**
|
|
318
|
+
* Called when transport error occurs (network failure, timeout, cancelled)
|
|
319
|
+
* @param url - Request URL
|
|
320
|
+
* @param error - Error detail with code and message
|
|
321
|
+
* @param metrics - Request timing metrics (optional)
|
|
322
|
+
*/
|
|
323
|
+
onError?: (url: string, error: NetworkErrorDetail, metrics?: RequestMetrics) => void;
|
|
324
|
+
/**
|
|
325
|
+
* Called when worker is ready after initialization
|
|
326
|
+
*/
|
|
327
|
+
onWorkerReady?: () => void;
|
|
328
|
+
/**
|
|
329
|
+
* Called when worker encounters a fatal error
|
|
330
|
+
* @param error - Error event from worker
|
|
331
|
+
*/
|
|
332
|
+
onWorkerError?: (error: ErrorEvent) => void;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Retry configuration for network errors
|
|
336
|
+
*
|
|
337
|
+
* Only retries on transport failures (network error, timeout).
|
|
338
|
+
* Does NOT retry on HTTP errors (4xx/5xx) - those are valid responses.
|
|
339
|
+
* Does NOT retry cancelled requests.
|
|
340
|
+
*/
|
|
341
|
+
interface RetryConfig {
|
|
342
|
+
/**
|
|
343
|
+
* Maximum number of retry attempts (default: 0 = no retry)
|
|
344
|
+
*/
|
|
345
|
+
maxAttempts?: number;
|
|
346
|
+
/**
|
|
347
|
+
* Delay between retries in milliseconds (default: 1000)
|
|
348
|
+
*/
|
|
349
|
+
delay?: number;
|
|
350
|
+
/**
|
|
351
|
+
* Exponential backoff multiplier (default: 1 = no backoff)
|
|
352
|
+
* Example: delay=1000, backoff=2 => 1s, 2s, 4s, 8s...
|
|
353
|
+
*/
|
|
354
|
+
backoff?: number;
|
|
355
|
+
/**
|
|
356
|
+
* Maximum delay in milliseconds (default: 30000)
|
|
357
|
+
* Caps the delay when using exponential backoff
|
|
358
|
+
*/
|
|
359
|
+
maxDelay?: number;
|
|
360
|
+
/**
|
|
361
|
+
* Jitter factor to add randomness to retry delays (default: 0 = no jitter)
|
|
362
|
+
* Range: 0 to 1 (e.g., 0.5 = ±50% randomness)
|
|
363
|
+
* Helps prevent thundering herd when many clients retry simultaneously.
|
|
364
|
+
*
|
|
365
|
+
* Note: Jitter is only applied when shouldRetry returns true.
|
|
366
|
+
* If request fails permanently, no jitter delay occurs.
|
|
367
|
+
*
|
|
368
|
+
* Example: delay=1000, jitter=0.5 => delay between 500ms and 1500ms
|
|
369
|
+
*/
|
|
370
|
+
jitter?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Custom condition to determine if error should be retried
|
|
373
|
+
* Default: retry on NETWORK_ERROR only
|
|
374
|
+
* @param error - The error that occurred
|
|
375
|
+
* @returns true to retry, false to fail immediately
|
|
376
|
+
*/
|
|
377
|
+
shouldRetry?: (error: NetworkErrorDetail) => boolean;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Request deduplication configuration
|
|
381
|
+
*
|
|
382
|
+
* When enabled, identical GET requests (same URL) within a time window
|
|
383
|
+
* will share the same in-flight request instead of making duplicates.
|
|
384
|
+
*
|
|
385
|
+
* IMPORTANT:
|
|
386
|
+
* - Only applies to GET requests (POST/PUT/DELETE are never deduplicated)
|
|
387
|
+
* - Only deduplicates in-flight requests (not caching)
|
|
388
|
+
* - Safe for most read operations
|
|
389
|
+
*/
|
|
390
|
+
interface DedupeConfig {
|
|
391
|
+
/**
|
|
392
|
+
* Enable deduplication (default: false)
|
|
393
|
+
*/
|
|
394
|
+
enabled?: boolean;
|
|
395
|
+
/**
|
|
396
|
+
* Time window in milliseconds to consider requests as duplicates (default: 0)
|
|
397
|
+
* 0 = only dedupe concurrent/in-flight requests
|
|
398
|
+
* >0 = also dedupe requests within this time window after completion
|
|
399
|
+
*/
|
|
400
|
+
window?: number;
|
|
401
|
+
/**
|
|
402
|
+
* Custom key generator for deduplication
|
|
403
|
+
* Default: uses URL only for GET requests
|
|
404
|
+
* @param url - Request URL
|
|
405
|
+
* @param options - Request options
|
|
406
|
+
* @returns Key string, or null to skip deduplication for this request
|
|
407
|
+
*/
|
|
408
|
+
keyGenerator?: (url: string, options: FetchGuardRequestInit) => string | null;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Transport result - represents the outcome of a network request
|
|
412
|
+
*
|
|
413
|
+
* IMPORTANT: This is a TRANSPORT result, not a business result.
|
|
414
|
+
* - ok = HTTP response received (check envelope.status for 2xx/4xx/5xx)
|
|
415
|
+
* - err = Network failure (no response received)
|
|
416
|
+
*
|
|
417
|
+
* Example:
|
|
418
|
+
* ```typescript
|
|
419
|
+
* const result = await api.get('/users')
|
|
420
|
+
* if (result.ok) {
|
|
421
|
+
* // Transport succeeded - got HTTP response
|
|
422
|
+
* if (result.data.status >= 200 && result.data.status < 400) {
|
|
423
|
+
* // Business success
|
|
424
|
+
* } else {
|
|
425
|
+
* // Business error (4xx/5xx) - still has response body
|
|
426
|
+
* }
|
|
427
|
+
* } else {
|
|
428
|
+
* // Transport failed - no response (network error, timeout, cancelled)
|
|
429
|
+
* }
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
type TransportResult = Result<FetchEnvelope>;
|
|
197
433
|
|
|
198
434
|
/**
|
|
199
435
|
* FetchGuard Client - main interface cho việc gọi API thông qua Web Worker
|
|
@@ -202,14 +438,28 @@ declare class FetchGuardClient {
|
|
|
202
438
|
private worker;
|
|
203
439
|
private messageId;
|
|
204
440
|
private pendingRequests;
|
|
441
|
+
/** Track request URLs for debug hooks */
|
|
442
|
+
private requestUrls;
|
|
443
|
+
/** Track request timing for metrics */
|
|
444
|
+
private requestTimings;
|
|
205
445
|
private authListeners;
|
|
206
446
|
private readyListeners;
|
|
207
447
|
private isReady;
|
|
208
448
|
private requestQueue;
|
|
209
|
-
private
|
|
210
|
-
private
|
|
449
|
+
private activeRequests;
|
|
450
|
+
private readonly maxConcurrent;
|
|
451
|
+
private readonly maxQueueSize;
|
|
452
|
+
private readonly setupTimeout;
|
|
453
|
+
private readonly requestTimeout;
|
|
211
454
|
private setupResolve?;
|
|
212
455
|
private setupReject?;
|
|
456
|
+
private readonly debug?;
|
|
457
|
+
private readonly retry?;
|
|
458
|
+
private readonly dedupe?;
|
|
459
|
+
/** In-flight requests for deduplication */
|
|
460
|
+
private readonly inFlightRequests;
|
|
461
|
+
/** Recent completed requests for time-window deduplication */
|
|
462
|
+
private readonly recentResults;
|
|
213
463
|
constructor(options: FetchGuardOptions);
|
|
214
464
|
/**
|
|
215
465
|
* Initialize worker with config and provider
|
|
@@ -228,9 +478,57 @@ declare class FetchGuardClient {
|
|
|
228
478
|
*/
|
|
229
479
|
private generateMessageId;
|
|
230
480
|
/**
|
|
231
|
-
* Make API request
|
|
481
|
+
* Make API request with optional deduplication, retry, and AbortSignal support
|
|
482
|
+
*
|
|
483
|
+
* @param url - Full URL to fetch
|
|
484
|
+
* @param options - Request options including optional AbortSignal
|
|
485
|
+
* @returns Result with FetchEnvelope on success, error on failure
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* // With AbortSignal
|
|
489
|
+
* const controller = new AbortController()
|
|
490
|
+
* setTimeout(() => controller.abort(), 5000)
|
|
491
|
+
* const result = await api.fetch('/slow', { signal: controller.signal })
|
|
232
492
|
*/
|
|
233
|
-
fetch(url: string, options?: FetchGuardRequestInit): Promise<Result<
|
|
493
|
+
fetch(url: string, options?: FetchGuardRequestInit): Promise<Result<FetchEnvelope>>;
|
|
494
|
+
/**
|
|
495
|
+
* Wrap a promise with AbortSignal support
|
|
496
|
+
*/
|
|
497
|
+
private wrapWithAbortSignal;
|
|
498
|
+
/**
|
|
499
|
+
* Fetch with retry logic and AbortSignal support (internal)
|
|
500
|
+
*/
|
|
501
|
+
private fetchWithRetryAndSignal;
|
|
502
|
+
/**
|
|
503
|
+
* Generate deduplication key for request
|
|
504
|
+
* Returns null if request should not be deduplicated
|
|
505
|
+
*/
|
|
506
|
+
private getDedupeKey;
|
|
507
|
+
/**
|
|
508
|
+
* Apply jitter to a delay value
|
|
509
|
+
* Jitter adds ±(jitter * delay) randomness to prevent thundering herd
|
|
510
|
+
* @param delay - Base delay in milliseconds
|
|
511
|
+
* @param jitter - Jitter factor (0-1)
|
|
512
|
+
* @returns Jittered delay
|
|
513
|
+
*/
|
|
514
|
+
private applyJitter;
|
|
515
|
+
/**
|
|
516
|
+
* Default retry condition - only retry on NETWORK_ERROR
|
|
517
|
+
*/
|
|
518
|
+
private defaultShouldRetry;
|
|
519
|
+
/**
|
|
520
|
+
* Calculate request metrics from timing data
|
|
521
|
+
*/
|
|
522
|
+
private calculateMetrics;
|
|
523
|
+
/**
|
|
524
|
+
* Sleep helper for retry delay
|
|
525
|
+
*/
|
|
526
|
+
private sleep;
|
|
527
|
+
/**
|
|
528
|
+
* Sleep with abort signal support
|
|
529
|
+
* Returns true if aborted, false if completed normally
|
|
530
|
+
*/
|
|
531
|
+
private sleepWithAbort;
|
|
234
532
|
/**
|
|
235
533
|
* Fetch with id for external cancellation
|
|
236
534
|
* Returns { id, result, cancel }
|
|
@@ -238,7 +536,7 @@ declare class FetchGuardClient {
|
|
|
238
536
|
*/
|
|
239
537
|
fetchWithId(url: string, options?: FetchGuardRequestInit): {
|
|
240
538
|
id: string;
|
|
241
|
-
result: Promise<Result<
|
|
539
|
+
result: Promise<Result<FetchEnvelope>>;
|
|
242
540
|
cancel: () => void;
|
|
243
541
|
};
|
|
244
542
|
/**
|
|
@@ -248,11 +546,11 @@ declare class FetchGuardClient {
|
|
|
248
546
|
/**
|
|
249
547
|
* Convenience methods
|
|
250
548
|
*/
|
|
251
|
-
get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
252
|
-
post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
253
|
-
put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
254
|
-
delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
255
|
-
patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<
|
|
549
|
+
get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
550
|
+
post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
551
|
+
put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
552
|
+
delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
553
|
+
patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
|
|
256
554
|
/**
|
|
257
555
|
* Generic method to call any auth method on provider
|
|
258
556
|
* @param method - Method name (login, logout, loginWithPhone, etc.)
|
|
@@ -304,16 +602,24 @@ declare class FetchGuardClient {
|
|
|
304
602
|
/**
|
|
305
603
|
* Send message through queue system
|
|
306
604
|
* All messages go through queue for sequential processing
|
|
605
|
+
* @param transferables - Optional Transferable objects for zero-copy postMessage
|
|
307
606
|
*/
|
|
308
607
|
private sendMessageQueued;
|
|
309
608
|
/**
|
|
310
|
-
* Process message queue
|
|
609
|
+
* Process message queue with concurrency limit
|
|
610
|
+
*
|
|
611
|
+
* Uses semaphore pattern to allow N concurrent requests.
|
|
311
612
|
* Benefits:
|
|
312
|
-
* -
|
|
613
|
+
* - Higher throughput than sequential processing
|
|
614
|
+
* - Backpressure via maxConcurrent limit
|
|
313
615
|
* - Better error isolation (one failure doesn't affect others)
|
|
314
|
-
* - 50ms delay between requests for backpressure
|
|
315
616
|
*/
|
|
316
617
|
private processQueue;
|
|
618
|
+
/**
|
|
619
|
+
* Called when a request completes (success or error)
|
|
620
|
+
* Decrements active count and processes next items in queue
|
|
621
|
+
*/
|
|
622
|
+
private onRequestComplete;
|
|
317
623
|
/**
|
|
318
624
|
* Cleanup - terminate worker
|
|
319
625
|
*/
|
|
@@ -387,11 +693,14 @@ interface WorkerPayloads {
|
|
|
387
693
|
};
|
|
388
694
|
AUTH_STATE_CHANGED: AuthResult;
|
|
389
695
|
AUTH_CALL_RESULT: AuthResult;
|
|
390
|
-
FETCH_RESULT:
|
|
696
|
+
FETCH_RESULT: FetchEnvelope;
|
|
391
697
|
FETCH_ERROR: {
|
|
392
698
|
error: string;
|
|
393
699
|
status?: number;
|
|
394
700
|
};
|
|
701
|
+
TOKEN_REFRESHED: {
|
|
702
|
+
reason: RefreshReason;
|
|
703
|
+
};
|
|
395
704
|
}
|
|
396
705
|
/**
|
|
397
706
|
* Generate message type from payload definition
|
|
@@ -456,8 +765,8 @@ declare const AuthErrors: {
|
|
|
456
765
|
*/
|
|
457
766
|
declare const DomainErrors: {
|
|
458
767
|
readonly NotAllowed: (params?: ({
|
|
459
|
-
url:
|
|
460
|
-
} & Partial<
|
|
768
|
+
url: string | number;
|
|
769
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
461
770
|
};
|
|
462
771
|
/**
|
|
463
772
|
* Request/Response errors (network, HTTP, parsing)
|
|
@@ -466,10 +775,55 @@ declare const RequestErrors: {
|
|
|
466
775
|
readonly NetworkError: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
467
776
|
readonly Cancelled: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
468
777
|
readonly HttpError: (params?: ({
|
|
469
|
-
status:
|
|
470
|
-
} & Partial<
|
|
778
|
+
status: string | number;
|
|
779
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
471
780
|
readonly ResponseParseFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
781
|
+
readonly QueueFull: (params?: ({
|
|
782
|
+
size: string | number;
|
|
783
|
+
maxSize: string | number;
|
|
784
|
+
} & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
|
|
785
|
+
readonly Timeout: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Error codes as constants for type-safe error matching
|
|
790
|
+
*
|
|
791
|
+
* Usage:
|
|
792
|
+
* ```typescript
|
|
793
|
+
* import { ERROR_CODES } from 'fetchguard'
|
|
794
|
+
*
|
|
795
|
+
* if (result.errors[0]?.code === ERROR_CODES.NETWORK_ERROR) {
|
|
796
|
+
* // Handle network error
|
|
797
|
+
* }
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
declare const ERROR_CODES: {
|
|
801
|
+
readonly UNEXPECTED: "UNEXPECTED";
|
|
802
|
+
readonly UNKNOWN_MESSAGE: "UNKNOWN_MESSAGE";
|
|
803
|
+
readonly RESULT_PARSE_ERROR: "RESULT_PARSE_ERROR";
|
|
804
|
+
readonly INIT_ERROR: "INIT_ERROR";
|
|
805
|
+
readonly PROVIDER_INIT_FAILED: "PROVIDER_INIT_FAILED";
|
|
806
|
+
readonly INIT_FAILED: "INIT_FAILED";
|
|
807
|
+
readonly TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED";
|
|
808
|
+
readonly LOGIN_FAILED: "LOGIN_FAILED";
|
|
809
|
+
readonly LOGOUT_FAILED: "LOGOUT_FAILED";
|
|
810
|
+
readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
|
|
811
|
+
readonly DOMAIN_NOT_ALLOWED: "DOMAIN_NOT_ALLOWED";
|
|
812
|
+
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
813
|
+
readonly REQUEST_CANCELLED: "REQUEST_CANCELLED";
|
|
814
|
+
readonly HTTP_ERROR: "HTTP_ERROR";
|
|
815
|
+
readonly RESPONSE_PARSE_FAILED: "RESPONSE_PARSE_FAILED";
|
|
816
|
+
readonly QUEUE_FULL: "QUEUE_FULL";
|
|
817
|
+
readonly REQUEST_TIMEOUT: "REQUEST_TIMEOUT";
|
|
472
818
|
};
|
|
819
|
+
/**
|
|
820
|
+
* Union type of all error code values
|
|
821
|
+
*/
|
|
822
|
+
type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
|
|
823
|
+
/**
|
|
824
|
+
* Union type of all error code keys (useful for telemetry mapping)
|
|
825
|
+
*/
|
|
826
|
+
type ErrorCodeKey = keyof typeof ERROR_CODES;
|
|
473
827
|
|
|
474
828
|
/**
|
|
475
829
|
* Register a token provider with name
|
|
@@ -531,12 +885,30 @@ interface ProviderConfig {
|
|
|
531
885
|
*/
|
|
532
886
|
declare function createProvider(config: ProviderConfig): TokenProvider;
|
|
533
887
|
|
|
888
|
+
/**
|
|
889
|
+
* IndexedDB storage options
|
|
890
|
+
*/
|
|
891
|
+
interface IndexedDBStorageOptions {
|
|
892
|
+
/** Database name (default: 'FetchGuardDB') */
|
|
893
|
+
dbName?: string;
|
|
894
|
+
/** Key for refresh token (default: 'refreshToken') */
|
|
895
|
+
refreshTokenKey?: string;
|
|
896
|
+
/**
|
|
897
|
+
* Error callback for debugging storage failures
|
|
898
|
+
* Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
|
|
899
|
+
* Storage still fails closed (returns null), but this allows logging/debugging.
|
|
900
|
+
*/
|
|
901
|
+
onError?: StorageErrorCallback;
|
|
902
|
+
}
|
|
534
903
|
/**
|
|
535
904
|
* IndexedDB storage - only stores refresh token in IndexedDB
|
|
536
905
|
* Suitable for body-based refresh strategy
|
|
537
906
|
* Persists refresh token for reuse after reload
|
|
907
|
+
*
|
|
908
|
+
* @param options - Storage options or legacy dbName string
|
|
909
|
+
* @param legacyRefreshTokenKey - Legacy refreshTokenKey (for backward compatibility)
|
|
538
910
|
*/
|
|
539
|
-
declare function createIndexedDBStorage(
|
|
911
|
+
declare function createIndexedDBStorage(options?: IndexedDBStorageOptions | string, legacyRefreshTokenKey?: string): RefreshTokenStorage;
|
|
540
912
|
|
|
541
913
|
/**
|
|
542
914
|
* Body parser - parse token from response body (JSON)
|
|
@@ -638,15 +1010,18 @@ declare function createBodyProvider(config: {
|
|
|
638
1010
|
|
|
639
1011
|
/**
|
|
640
1012
|
* Serialize FormData for transfer over postMessage
|
|
641
|
-
* Inspired by api-worker.js:484-518
|
|
642
1013
|
*
|
|
643
|
-
* FormData cannot be cloned via postMessage, so we need to serialize it first
|
|
644
|
-
* Files are converted to ArrayBuffer
|
|
1014
|
+
* FormData cannot be cloned via postMessage, so we need to serialize it first.
|
|
1015
|
+
* Files are converted to ArrayBuffer and returned as transferables for zero-copy transfer.
|
|
1016
|
+
*
|
|
1017
|
+
* IMPORTANT: Preserves original field order by using single-pass iteration.
|
|
1018
|
+
*
|
|
1019
|
+
* @returns SerializedFormDataResult with data and transferables array
|
|
645
1020
|
*/
|
|
646
|
-
declare function serializeFormData(formData: FormData): Promise<
|
|
1021
|
+
declare function serializeFormData(formData: FormData): Promise<SerializedFormDataResult>;
|
|
647
1022
|
/**
|
|
648
1023
|
* Deserialize SerializedFormData back to FormData in worker
|
|
649
|
-
* Reconstructs File objects from
|
|
1024
|
+
* Reconstructs File objects from transferred ArrayBuffers
|
|
650
1025
|
*/
|
|
651
1026
|
declare function deserializeFormData(serialized: SerializedFormData): FormData;
|
|
652
1027
|
/**
|
|
@@ -669,4 +1044,138 @@ declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
|
|
|
669
1044
|
*/
|
|
670
1045
|
declare function isBinaryContentType(contentType: string): boolean;
|
|
671
1046
|
|
|
672
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* Helper functions for common Result patterns
|
|
1049
|
+
*
|
|
1050
|
+
* These utilities simplify working with FetchGuard's Result-based API
|
|
1051
|
+
* by providing type-safe helpers for common operations.
|
|
1052
|
+
*/
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Check if result is a network/transport error (not an HTTP response)
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* const result = await api.fetch('/users')
|
|
1059
|
+
* if (isNetworkError(result)) {
|
|
1060
|
+
* console.error('Network failed:', result.errors[0]?.message)
|
|
1061
|
+
* }
|
|
1062
|
+
*/
|
|
1063
|
+
declare function isNetworkError(result: Result<FetchEnvelope>): result is Result<FetchEnvelope> & {
|
|
1064
|
+
ok: false;
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Check if response is successful (2xx status)
|
|
1068
|
+
*
|
|
1069
|
+
* @example
|
|
1070
|
+
* if (isSuccess(result)) {
|
|
1071
|
+
* const data = parseJson(result)
|
|
1072
|
+
* }
|
|
1073
|
+
*/
|
|
1074
|
+
declare function isSuccess(result: Result<FetchEnvelope>): boolean;
|
|
1075
|
+
/**
|
|
1076
|
+
* Check if response is a client error (4xx status)
|
|
1077
|
+
*
|
|
1078
|
+
* @example
|
|
1079
|
+
* if (isClientError(result)) {
|
|
1080
|
+
* console.error('Bad request:', getErrorMessage(result))
|
|
1081
|
+
* }
|
|
1082
|
+
*/
|
|
1083
|
+
declare function isClientError(result: Result<FetchEnvelope>): boolean;
|
|
1084
|
+
/**
|
|
1085
|
+
* Check if response is a server error (5xx status)
|
|
1086
|
+
*
|
|
1087
|
+
* @example
|
|
1088
|
+
* if (isServerError(result)) {
|
|
1089
|
+
* // Maybe retry?
|
|
1090
|
+
* }
|
|
1091
|
+
*/
|
|
1092
|
+
declare function isServerError(result: Result<FetchEnvelope>): boolean;
|
|
1093
|
+
/**
|
|
1094
|
+
* Parse JSON body safely with optional type inference
|
|
1095
|
+
*
|
|
1096
|
+
* Returns null if:
|
|
1097
|
+
* - Result is a network error (no response)
|
|
1098
|
+
* - Body is not valid JSON
|
|
1099
|
+
*
|
|
1100
|
+
* @example
|
|
1101
|
+
* const users = parseJson<User[]>(result)
|
|
1102
|
+
* if (users) {
|
|
1103
|
+
* // Use users array
|
|
1104
|
+
* }
|
|
1105
|
+
*/
|
|
1106
|
+
declare function parseJson<T = unknown>(result: Result<FetchEnvelope>): T | null;
|
|
1107
|
+
/**
|
|
1108
|
+
* Get human-readable error message from result
|
|
1109
|
+
*
|
|
1110
|
+
* For network errors: returns the error message
|
|
1111
|
+
* For HTTP errors: tries to parse message from body, falls back to status code
|
|
1112
|
+
*
|
|
1113
|
+
* @example
|
|
1114
|
+
* if (!isSuccess(result)) {
|
|
1115
|
+
* toast.error(getErrorMessage(result))
|
|
1116
|
+
* }
|
|
1117
|
+
*/
|
|
1118
|
+
declare function getErrorMessage(result: Result<FetchEnvelope>): string;
|
|
1119
|
+
/**
|
|
1120
|
+
* Get error body with type safety (best-effort parsing)
|
|
1121
|
+
*
|
|
1122
|
+
* NOTE: This is best-effort parsing. The error body comes from the server
|
|
1123
|
+
* and may not match the expected shape. Always handle null return and
|
|
1124
|
+
* validate before using typed properties.
|
|
1125
|
+
*
|
|
1126
|
+
* @example
|
|
1127
|
+
* interface ApiError {
|
|
1128
|
+
* code: string
|
|
1129
|
+
* message: string
|
|
1130
|
+
* errors?: { field: string; message: string }[]
|
|
1131
|
+
* }
|
|
1132
|
+
*
|
|
1133
|
+
* const errorBody = getErrorBody<ApiError>(result)
|
|
1134
|
+
* if (errorBody?.errors) {
|
|
1135
|
+
* errorBody.errors.forEach(e => console.error(`${e.field}: ${e.message}`))
|
|
1136
|
+
* }
|
|
1137
|
+
*/
|
|
1138
|
+
declare function getErrorBody<T = unknown>(result: Result<FetchEnvelope>): T | null;
|
|
1139
|
+
/**
|
|
1140
|
+
* Get the HTTP status code from result
|
|
1141
|
+
*
|
|
1142
|
+
* Returns null if result is a network error (no HTTP response)
|
|
1143
|
+
*
|
|
1144
|
+
* @example
|
|
1145
|
+
* const status = getStatus(result)
|
|
1146
|
+
* if (status === 401) {
|
|
1147
|
+
* // Redirect to login
|
|
1148
|
+
* }
|
|
1149
|
+
*/
|
|
1150
|
+
declare function getStatus(result: Result<FetchEnvelope>): number | null;
|
|
1151
|
+
/**
|
|
1152
|
+
* Check if result has a specific HTTP status
|
|
1153
|
+
*
|
|
1154
|
+
* @example
|
|
1155
|
+
* if (hasStatus(result, 404)) {
|
|
1156
|
+
* console.log('Not found')
|
|
1157
|
+
* }
|
|
1158
|
+
*/
|
|
1159
|
+
declare function hasStatus(result: Result<FetchEnvelope>, status: number): boolean;
|
|
1160
|
+
/**
|
|
1161
|
+
* Match result against multiple handlers
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* matchResult(result, {
|
|
1165
|
+
* success: (data) => console.log('Success:', data),
|
|
1166
|
+
* clientError: (data) => console.error('Client error:', data.status),
|
|
1167
|
+
* serverError: (data) => console.error('Server error:', data.status),
|
|
1168
|
+
* networkError: (errors) => console.error('Network:', errors[0]?.message)
|
|
1169
|
+
* })
|
|
1170
|
+
*/
|
|
1171
|
+
declare function matchResult<T>(result: Result<FetchEnvelope>, handlers: {
|
|
1172
|
+
success?: (data: FetchEnvelope) => T;
|
|
1173
|
+
clientError?: (data: FetchEnvelope) => T;
|
|
1174
|
+
serverError?: (data: FetchEnvelope) => T;
|
|
1175
|
+
networkError?: (errors: readonly {
|
|
1176
|
+
code: string;
|
|
1177
|
+
message: string;
|
|
1178
|
+
}[]) => T;
|
|
1179
|
+
}): T | undefined;
|
|
1180
|
+
|
|
1181
|
+
export { AuthErrors, type AuthResult, type AuthStrategy, type DebugHooks, type DedupeConfig, DomainErrors, ERROR_CODES, type ErrorCode, type ErrorCodeKey, type FetchEnvelope, FetchGuardClient, type FetchGuardOptions, type FetchGuardRequestInit, GeneralErrors, type IndexedDBStorageOptions, InitErrors, MSG, type MainToWorkerMessage, type MessageType, type NetworkErrorDetail, type ProviderConfig, type ProviderPresetConfig, type RefreshReason, type RefreshTokenStorage, RequestErrors, type RequestMetrics, type RetryConfig, type SerializedFile, type SerializedFormData, type SerializedFormDataEntry, type StorageErrorCallback, type StorageErrorContext, type TokenInfo, type TokenParser, type TokenProvider, type TransportResult, type WorkerConfig, type WorkerToMainMessage, base64ToArrayBuffer, bodyParser, bodyStrategy, clearProviders, cookieParser, cookieStrategy, createBodyProvider, createBodyStrategy, createClient, createCookieProvider, createCookieStrategy, createIndexedDBStorage, createProvider, deserializeFormData, getErrorBody, getErrorMessage, getProvider, getStatus, hasProvider, hasStatus, isBinaryContentType, isClientError, isFormData, isNetworkError, isSerializedFormData, isServerError, isSuccess, listProviders, matchResult, parseJson, registerProvider, serializeFormData, unregisterProvider };
|