fetchguard 1.6.3 → 2.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/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
@@ -23,6 +22,18 @@ interface TokenInfo {
23
22
  refreshToken?: string | null;
24
23
  user?: unknown;
25
24
  }
25
+ /**
26
+ * Options for token exchange operation
27
+ *
28
+ * Used when switching tenant, changing scope, or any operation
29
+ * that exchanges current token for a new one with different claims
30
+ */
31
+ interface ExchangeTokenOptions {
32
+ /** HTTP method to use. Default: 'POST' */
33
+ method?: 'POST' | 'PUT';
34
+ /** Payload to send with the request (e.g., tenantId, scope) */
35
+ payload?: Record<string, unknown>;
36
+ }
26
37
  /**
27
38
  * Auth result returned from auth operations and auth state changes
28
39
  * Used by: login(), logout(), refreshToken(), onAuthStateChanged()
@@ -66,6 +77,18 @@ interface TokenProvider {
66
77
  * @returns Result<TokenInfo> with all fields reset (token = '', refreshToken = undefined, user = undefined)
67
78
  */
68
79
  logout(payload?: unknown): Promise<Result<TokenInfo>>;
80
+ /**
81
+ * Exchange current token for a new one with different context
82
+ *
83
+ * Useful for switching tenants, changing scopes, or any operation
84
+ * that requires exchanging the current token for a new one.
85
+ *
86
+ * @param accessToken - Current access token (injected by worker)
87
+ * @param url - URL to call for token exchange
88
+ * @param options - Exchange options (method, payload)
89
+ * @returns Result<TokenInfo> with new tokens
90
+ */
91
+ exchangeToken(accessToken: string, url: string, options?: ExchangeTokenOptions): Promise<Result<TokenInfo>>;
69
92
  /**
70
93
  * Custom auth methods (optional)
71
94
  * Examples: loginWithPhone, loginWithGoogle, loginWithFacebook, etc.
@@ -76,6 +99,16 @@ interface TokenProvider {
76
99
  */
77
100
  [key: string]: (...args: any[]) => Promise<Result<TokenInfo>>;
78
101
  }
102
+ /**
103
+ * Storage error context for debugging
104
+ */
105
+ type StorageErrorContext = 'get' | 'set' | 'delete' | 'open';
106
+ /**
107
+ * Storage error callback type
108
+ * Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
109
+ * Storage still fails closed (returns null), but this allows logging/debugging.
110
+ */
111
+ type StorageErrorCallback = (error: Error, context: StorageErrorContext) => void;
79
112
  /**
80
113
  * Interface for refresh token storage - only stores refresh token
81
114
  *
@@ -144,6 +177,46 @@ interface FetchGuardOptions {
144
177
  refreshEarlyMs?: number;
145
178
  /** Default headers to include in all requests */
146
179
  defaultHeaders?: Record<string, string>;
180
+ /**
181
+ * Maximum concurrent requests to worker (default: 6)
182
+ * Controls how many requests can be in-flight simultaneously.
183
+ * Set to 1 for strictly sequential processing.
184
+ * Higher values increase throughput but may cause worker congestion.
185
+ */
186
+ maxConcurrent?: number;
187
+ /**
188
+ * Maximum queue size for pending requests (default: 1000)
189
+ * When queue is full, new requests will immediately fail with QUEUE_FULL error.
190
+ * Prevents memory leak if worker is unresponsive.
191
+ */
192
+ maxQueueSize?: number;
193
+ /**
194
+ * Worker setup timeout in milliseconds (default: 10000)
195
+ * How long to wait for worker to be ready before failing.
196
+ */
197
+ setupTimeout?: number;
198
+ /**
199
+ * Default request timeout in milliseconds (default: 30000)
200
+ * How long to wait for a request to complete before timing out.
201
+ * Can be overridden per-request via fetch options.
202
+ */
203
+ requestTimeout?: number;
204
+ /**
205
+ * Debug hooks for observing operations (logging, monitoring)
206
+ * All hooks are observe-only - they cannot modify requests/responses.
207
+ */
208
+ debug?: DebugHooks;
209
+ /**
210
+ * Retry configuration for network errors
211
+ * Only retries on transport failures, NOT on HTTP errors (4xx/5xx)
212
+ */
213
+ retry?: RetryConfig;
214
+ /**
215
+ * Request deduplication configuration
216
+ * When enabled, duplicate requests to the same URL within a time window
217
+ * will share the same response instead of making multiple requests.
218
+ */
219
+ dedupe?: DedupeConfig;
147
220
  }
148
221
  /**
149
222
  * Internal worker configuration
@@ -163,25 +236,31 @@ interface FetchGuardRequestInit extends RequestInit {
163
236
  includeHeaders?: boolean;
164
237
  }
165
238
  /**
166
- * API response structure
239
+ * Fetch envelope - raw HTTP response from worker
240
+ *
241
+ * Worker only fetches and returns raw data, does NOT judge HTTP status.
242
+ * Client receives envelope and decides ok/err based on business logic.
243
+ *
244
+ * - status: HTTP status code (2xx, 3xx, 4xx, 5xx)
167
245
  * - body: string (text/JSON) or base64 (binary)
168
246
  * - contentType: always present, indicates how to decode body
169
247
  * - headers: empty object if includeHeaders: false
170
- * - status: HTTP status code
171
248
  */
172
- interface ApiResponse {
173
- body: string;
249
+ interface FetchEnvelope {
174
250
  status: number;
251
+ body: string;
175
252
  contentType: string;
176
253
  headers: Record<string, string>;
177
254
  }
178
255
  /**
179
256
  * Serialized file data for transfer over postMessage
257
+ * Uses ArrayBuffer for zero-copy transfer via Transferable
180
258
  */
181
259
  interface SerializedFile {
182
260
  name: string;
183
261
  type: string;
184
- data: number[];
262
+ /** ArrayBuffer - transferred via postMessage Transferable for zero-copy */
263
+ buffer: ArrayBuffer;
185
264
  }
186
265
  /**
187
266
  * Serialized FormData entry - can be string or file
@@ -194,6 +273,187 @@ interface SerializedFormData {
194
273
  _type: 'FormData';
195
274
  entries: Array<[string, SerializedFormDataEntry]>;
196
275
  }
276
+ /**
277
+ * Result of FormData serialization with transferables
278
+ * Used for zero-copy transfer via postMessage
279
+ */
280
+ interface SerializedFormDataResult {
281
+ data: SerializedFormData;
282
+ /** ArrayBuffers to transfer - pass to postMessage as second argument */
283
+ transferables: ArrayBuffer[];
284
+ }
285
+ /**
286
+ * Network error detail for transport failures
287
+ * Used when no HTTP response is received (connection failed, timeout, cancelled)
288
+ */
289
+ interface NetworkErrorDetail {
290
+ code: 'NETWORK_ERROR' | 'REQUEST_CANCELLED' | 'RESPONSE_PARSE_FAILED';
291
+ message: string;
292
+ }
293
+ /**
294
+ * Reason for token refresh
295
+ */
296
+ type RefreshReason = 'expired' | 'proactive' | 'manual';
297
+ /**
298
+ * Request timing metrics for performance monitoring
299
+ *
300
+ * All times are in milliseconds.
301
+ */
302
+ interface RequestMetrics {
303
+ /** When request was initiated (Date.now()) */
304
+ startTime: number;
305
+ /** When response was received (Date.now()) */
306
+ endTime: number;
307
+ /** Total duration (endTime - startTime) */
308
+ duration: number;
309
+ /** Time spent waiting in queue before processing */
310
+ queueTime: number;
311
+ /** Time spent in IPC (postMessage round-trip overhead) */
312
+ ipcTime: number;
313
+ }
314
+ /**
315
+ * Debug hooks for observing FetchGuard operations
316
+ *
317
+ * All hooks are observe-only - they cannot modify requests/responses.
318
+ * Useful for logging, debugging, and monitoring.
319
+ *
320
+ * Note: Hooks run synchronously and should not perform heavy operations.
321
+ */
322
+ interface DebugHooks {
323
+ /**
324
+ * Called before each request is sent to worker
325
+ * @param url - Request URL
326
+ * @param options - Request options (method, headers, etc.)
327
+ */
328
+ onRequest?: (url: string, options: FetchGuardRequestInit) => void;
329
+ /**
330
+ * Called when response is received from worker
331
+ * @param url - Request URL
332
+ * @param envelope - Response envelope (status, body, headers)
333
+ * @param metrics - Request timing metrics (optional, for performance monitoring)
334
+ */
335
+ onResponse?: (url: string, envelope: FetchEnvelope, metrics?: RequestMetrics) => void;
336
+ /**
337
+ * Called when token refresh occurs
338
+ * @param reason - Why refresh happened: 'expired', 'proactive', or 'manual'
339
+ */
340
+ onRefresh?: (reason: RefreshReason) => void;
341
+ /**
342
+ * Called when transport error occurs (network failure, timeout, cancelled)
343
+ * @param url - Request URL
344
+ * @param error - Error detail with code and message
345
+ * @param metrics - Request timing metrics (optional)
346
+ */
347
+ onError?: (url: string, error: NetworkErrorDetail, metrics?: RequestMetrics) => void;
348
+ /**
349
+ * Called when worker is ready after initialization
350
+ */
351
+ onWorkerReady?: () => void;
352
+ /**
353
+ * Called when worker encounters a fatal error
354
+ * @param error - Error event from worker
355
+ */
356
+ onWorkerError?: (error: ErrorEvent) => void;
357
+ }
358
+ /**
359
+ * Retry configuration for network errors
360
+ *
361
+ * Only retries on transport failures (network error, timeout).
362
+ * Does NOT retry on HTTP errors (4xx/5xx) - those are valid responses.
363
+ * Does NOT retry cancelled requests.
364
+ */
365
+ interface RetryConfig {
366
+ /**
367
+ * Maximum number of retry attempts (default: 0 = no retry)
368
+ */
369
+ maxAttempts?: number;
370
+ /**
371
+ * Delay between retries in milliseconds (default: 1000)
372
+ */
373
+ delay?: number;
374
+ /**
375
+ * Exponential backoff multiplier (default: 1 = no backoff)
376
+ * Example: delay=1000, backoff=2 => 1s, 2s, 4s, 8s...
377
+ */
378
+ backoff?: number;
379
+ /**
380
+ * Maximum delay in milliseconds (default: 30000)
381
+ * Caps the delay when using exponential backoff
382
+ */
383
+ maxDelay?: number;
384
+ /**
385
+ * Jitter factor to add randomness to retry delays (default: 0 = no jitter)
386
+ * Range: 0 to 1 (e.g., 0.5 = ±50% randomness)
387
+ * Helps prevent thundering herd when many clients retry simultaneously.
388
+ *
389
+ * Note: Jitter is only applied when shouldRetry returns true.
390
+ * If request fails permanently, no jitter delay occurs.
391
+ *
392
+ * Example: delay=1000, jitter=0.5 => delay between 500ms and 1500ms
393
+ */
394
+ jitter?: number;
395
+ /**
396
+ * Custom condition to determine if error should be retried
397
+ * Default: retry on NETWORK_ERROR only
398
+ * @param error - The error that occurred
399
+ * @returns true to retry, false to fail immediately
400
+ */
401
+ shouldRetry?: (error: NetworkErrorDetail) => boolean;
402
+ }
403
+ /**
404
+ * Request deduplication configuration
405
+ *
406
+ * When enabled, identical GET requests (same URL) within a time window
407
+ * will share the same in-flight request instead of making duplicates.
408
+ *
409
+ * IMPORTANT:
410
+ * - Only applies to GET requests (POST/PUT/DELETE are never deduplicated)
411
+ * - Only deduplicates in-flight requests (not caching)
412
+ * - Safe for most read operations
413
+ */
414
+ interface DedupeConfig {
415
+ /**
416
+ * Enable deduplication (default: false)
417
+ */
418
+ enabled?: boolean;
419
+ /**
420
+ * Time window in milliseconds to consider requests as duplicates (default: 0)
421
+ * 0 = only dedupe concurrent/in-flight requests
422
+ * >0 = also dedupe requests within this time window after completion
423
+ */
424
+ window?: number;
425
+ /**
426
+ * Custom key generator for deduplication
427
+ * Default: uses URL only for GET requests
428
+ * @param url - Request URL
429
+ * @param options - Request options
430
+ * @returns Key string, or null to skip deduplication for this request
431
+ */
432
+ keyGenerator?: (url: string, options: FetchGuardRequestInit) => string | null;
433
+ }
434
+ /**
435
+ * Transport result - represents the outcome of a network request
436
+ *
437
+ * IMPORTANT: This is a TRANSPORT result, not a business result.
438
+ * - ok = HTTP response received (check envelope.status for 2xx/4xx/5xx)
439
+ * - err = Network failure (no response received)
440
+ *
441
+ * Example:
442
+ * ```typescript
443
+ * const result = await api.get('/users')
444
+ * if (result.ok) {
445
+ * // Transport succeeded - got HTTP response
446
+ * if (result.data.status >= 200 && result.data.status < 400) {
447
+ * // Business success
448
+ * } else {
449
+ * // Business error (4xx/5xx) - still has response body
450
+ * }
451
+ * } else {
452
+ * // Transport failed - no response (network error, timeout, cancelled)
453
+ * }
454
+ * ```
455
+ */
456
+ type TransportResult = Result<FetchEnvelope>;
197
457
 
198
458
  /**
199
459
  * FetchGuard Client - main interface cho việc gọi API thông qua Web Worker
@@ -202,14 +462,28 @@ declare class FetchGuardClient {
202
462
  private worker;
203
463
  private messageId;
204
464
  private pendingRequests;
465
+ /** Track request URLs for debug hooks */
466
+ private requestUrls;
467
+ /** Track request timing for metrics */
468
+ private requestTimings;
205
469
  private authListeners;
206
470
  private readyListeners;
207
471
  private isReady;
208
472
  private requestQueue;
209
- private isProcessingQueue;
210
- private queueTimeout;
473
+ private activeRequests;
474
+ private readonly maxConcurrent;
475
+ private readonly maxQueueSize;
476
+ private readonly setupTimeout;
477
+ private readonly requestTimeout;
211
478
  private setupResolve?;
212
479
  private setupReject?;
480
+ private readonly debug?;
481
+ private readonly retry?;
482
+ private readonly dedupe?;
483
+ /** In-flight requests for deduplication */
484
+ private readonly inFlightRequests;
485
+ /** Recent completed requests for time-window deduplication */
486
+ private readonly recentResults;
213
487
  constructor(options: FetchGuardOptions);
214
488
  /**
215
489
  * Initialize worker with config and provider
@@ -228,9 +502,57 @@ declare class FetchGuardClient {
228
502
  */
229
503
  private generateMessageId;
230
504
  /**
231
- * Make API request
505
+ * Make API request with optional deduplication, retry, and AbortSignal support
506
+ *
507
+ * @param url - Full URL to fetch
508
+ * @param options - Request options including optional AbortSignal
509
+ * @returns Result with FetchEnvelope on success, error on failure
510
+ *
511
+ * @example
512
+ * // With AbortSignal
513
+ * const controller = new AbortController()
514
+ * setTimeout(() => controller.abort(), 5000)
515
+ * const result = await api.fetch('/slow', { signal: controller.signal })
516
+ */
517
+ fetch(url: string, options?: FetchGuardRequestInit): Promise<Result<FetchEnvelope>>;
518
+ /**
519
+ * Wrap a promise with AbortSignal support
232
520
  */
233
- fetch(url: string, options?: FetchGuardRequestInit): Promise<Result<ApiResponse>>;
521
+ private wrapWithAbortSignal;
522
+ /**
523
+ * Fetch with retry logic and AbortSignal support (internal)
524
+ */
525
+ private fetchWithRetryAndSignal;
526
+ /**
527
+ * Generate deduplication key for request
528
+ * Returns null if request should not be deduplicated
529
+ */
530
+ private getDedupeKey;
531
+ /**
532
+ * Apply jitter to a delay value
533
+ * Jitter adds ±(jitter * delay) randomness to prevent thundering herd
534
+ * @param delay - Base delay in milliseconds
535
+ * @param jitter - Jitter factor (0-1)
536
+ * @returns Jittered delay
537
+ */
538
+ private applyJitter;
539
+ /**
540
+ * Default retry condition - only retry on NETWORK_ERROR
541
+ */
542
+ private defaultShouldRetry;
543
+ /**
544
+ * Calculate request metrics from timing data
545
+ */
546
+ private calculateMetrics;
547
+ /**
548
+ * Sleep helper for retry delay
549
+ */
550
+ private sleep;
551
+ /**
552
+ * Sleep with abort signal support
553
+ * Returns true if aborted, false if completed normally
554
+ */
555
+ private sleepWithAbort;
234
556
  /**
235
557
  * Fetch with id for external cancellation
236
558
  * Returns { id, result, cancel }
@@ -238,7 +560,7 @@ declare class FetchGuardClient {
238
560
  */
239
561
  fetchWithId(url: string, options?: FetchGuardRequestInit): {
240
562
  id: string;
241
- result: Promise<Result<ApiResponse>>;
563
+ result: Promise<Result<FetchEnvelope>>;
242
564
  cancel: () => void;
243
565
  };
244
566
  /**
@@ -248,11 +570,11 @@ declare class FetchGuardClient {
248
570
  /**
249
571
  * Convenience methods
250
572
  */
251
- get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<ApiResponse>>;
252
- post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<ApiResponse>>;
253
- put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<ApiResponse>>;
254
- delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<ApiResponse>>;
255
- patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<ApiResponse>>;
573
+ get(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
574
+ post(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
575
+ put(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
576
+ delete(url: string, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
577
+ patch(url: string, body?: unknown, options?: Omit<FetchGuardRequestInit, 'method' | 'body'>): Promise<Result<FetchEnvelope>>;
256
578
  /**
257
579
  * Generic method to call any auth method on provider
258
580
  * @param method - Method name (login, logout, loginWithPhone, etc.)
@@ -279,6 +601,34 @@ declare class FetchGuardClient {
279
601
  * @param emitEvent - Whether to emit AUTH_STATE_CHANGED event (default: true)
280
602
  */
281
603
  refreshToken(emitEvent?: boolean): Promise<Result<AuthResult>>;
604
+ /**
605
+ * Exchange current token for a new one with different context
606
+ *
607
+ * Useful for:
608
+ * - Switching tenants in multi-tenant apps
609
+ * - Changing authorization scope
610
+ * - Impersonating users (admin feature)
611
+ *
612
+ * @param url - URL to call for token exchange
613
+ * @param options - Exchange options (method, payload)
614
+ * @param emitEvent - Whether to emit AUTH_STATE_CHANGED event (default: true)
615
+ *
616
+ * @example
617
+ * // Switch tenant
618
+ * await api.exchangeToken('https://auth.example.com/auth/select-tenant', {
619
+ * payload: { tenantId: 'tenant_123' }
620
+ * })
621
+ *
622
+ * // Change scope with PUT method
623
+ * await api.exchangeToken('https://auth.example.com/auth/switch-context', {
624
+ * method: 'PUT',
625
+ * payload: { scope: 'admin' }
626
+ * })
627
+ */
628
+ exchangeToken(url: string, options?: {
629
+ method?: 'POST' | 'PUT';
630
+ payload?: Record<string, unknown>;
631
+ }, emitEvent?: boolean): Promise<Result<AuthResult>>;
282
632
  /**
283
633
  * Check if worker is ready
284
634
  */
@@ -304,16 +654,24 @@ declare class FetchGuardClient {
304
654
  /**
305
655
  * Send message through queue system
306
656
  * All messages go through queue for sequential processing
657
+ * @param transferables - Optional Transferable objects for zero-copy postMessage
307
658
  */
308
659
  private sendMessageQueued;
309
660
  /**
310
- * Process message queue sequentially
661
+ * Process message queue with concurrency limit
662
+ *
663
+ * Uses semaphore pattern to allow N concurrent requests.
311
664
  * Benefits:
312
- * - Sequential processing prevents worker overload
665
+ * - Higher throughput than sequential processing
666
+ * - Backpressure via maxConcurrent limit
313
667
  * - Better error isolation (one failure doesn't affect others)
314
- * - 50ms delay between requests for backpressure
315
668
  */
316
669
  private processQueue;
670
+ /**
671
+ * Called when a request completes (success or error)
672
+ * Decrements active count and processes next items in queue
673
+ */
674
+ private onRequestComplete;
317
675
  /**
318
676
  * Cleanup - terminate worker
319
677
  */
@@ -387,11 +745,14 @@ interface WorkerPayloads {
387
745
  };
388
746
  AUTH_STATE_CHANGED: AuthResult;
389
747
  AUTH_CALL_RESULT: AuthResult;
390
- FETCH_RESULT: ApiResponse;
748
+ FETCH_RESULT: FetchEnvelope;
391
749
  FETCH_ERROR: {
392
750
  error: string;
393
751
  status?: number;
394
752
  };
753
+ TOKEN_REFRESHED: {
754
+ reason: RefreshReason;
755
+ };
395
756
  }
396
757
  /**
397
758
  * Generate message type from payload definition
@@ -447,6 +808,7 @@ declare const InitErrors: {
447
808
  */
448
809
  declare const AuthErrors: {
449
810
  readonly TokenRefreshFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
811
+ readonly TokenExchangeFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
450
812
  readonly LoginFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
451
813
  readonly LogoutFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
452
814
  readonly NotAuthenticated: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
@@ -456,8 +818,8 @@ declare const AuthErrors: {
456
818
  */
457
819
  declare const DomainErrors: {
458
820
  readonly NotAllowed: (params?: ({
459
- url: any;
460
- } & Partial<ts_micro_result_factories_errors_advanced.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
821
+ url: string | number;
822
+ } & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
461
823
  };
462
824
  /**
463
825
  * Request/Response errors (network, HTTP, parsing)
@@ -466,11 +828,57 @@ declare const RequestErrors: {
466
828
  readonly NetworkError: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
467
829
  readonly Cancelled: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
468
830
  readonly HttpError: (params?: ({
469
- status: any;
470
- } & Partial<ts_micro_result_factories_errors_advanced.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
831
+ status: string | number;
832
+ } & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
471
833
  readonly ResponseParseFailed: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
834
+ readonly QueueFull: (params?: ({
835
+ size: string | number;
836
+ maxSize: string | number;
837
+ } & Partial<ts_micro_result.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
838
+ readonly Timeout: (params?: ts_micro_result.BaseErrorParams) => ts_micro_result.ErrorDetail;
472
839
  };
473
840
 
841
+ /**
842
+ * Error codes as constants for type-safe error matching
843
+ *
844
+ * Usage:
845
+ * ```typescript
846
+ * import { ERROR_CODES } from 'fetchguard'
847
+ *
848
+ * if (result.errors[0]?.code === ERROR_CODES.NETWORK_ERROR) {
849
+ * // Handle network error
850
+ * }
851
+ * ```
852
+ */
853
+ declare const ERROR_CODES: {
854
+ readonly UNEXPECTED: "UNEXPECTED";
855
+ readonly UNKNOWN_MESSAGE: "UNKNOWN_MESSAGE";
856
+ readonly RESULT_PARSE_ERROR: "RESULT_PARSE_ERROR";
857
+ readonly INIT_ERROR: "INIT_ERROR";
858
+ readonly PROVIDER_INIT_FAILED: "PROVIDER_INIT_FAILED";
859
+ readonly INIT_FAILED: "INIT_FAILED";
860
+ readonly TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED";
861
+ readonly TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED";
862
+ readonly LOGIN_FAILED: "LOGIN_FAILED";
863
+ readonly LOGOUT_FAILED: "LOGOUT_FAILED";
864
+ readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
865
+ readonly DOMAIN_NOT_ALLOWED: "DOMAIN_NOT_ALLOWED";
866
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
867
+ readonly REQUEST_CANCELLED: "REQUEST_CANCELLED";
868
+ readonly HTTP_ERROR: "HTTP_ERROR";
869
+ readonly RESPONSE_PARSE_FAILED: "RESPONSE_PARSE_FAILED";
870
+ readonly QUEUE_FULL: "QUEUE_FULL";
871
+ readonly REQUEST_TIMEOUT: "REQUEST_TIMEOUT";
872
+ };
873
+ /**
874
+ * Union type of all error code values
875
+ */
876
+ type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
877
+ /**
878
+ * Union type of all error code keys (useful for telemetry mapping)
879
+ */
880
+ type ErrorCodeKey = keyof typeof ERROR_CODES;
881
+
474
882
  /**
475
883
  * Register a token provider with name
476
884
  */
@@ -531,12 +939,30 @@ interface ProviderConfig {
531
939
  */
532
940
  declare function createProvider(config: ProviderConfig): TokenProvider;
533
941
 
942
+ /**
943
+ * IndexedDB storage options
944
+ */
945
+ interface IndexedDBStorageOptions {
946
+ /** Database name (default: 'FetchGuardDB') */
947
+ dbName?: string;
948
+ /** Key for refresh token (default: 'refreshToken') */
949
+ refreshTokenKey?: string;
950
+ /**
951
+ * Error callback for debugging storage failures
952
+ * Called when IndexedDB operations fail (quota exceeded, permission denied, etc.)
953
+ * Storage still fails closed (returns null), but this allows logging/debugging.
954
+ */
955
+ onError?: StorageErrorCallback;
956
+ }
534
957
  /**
535
958
  * IndexedDB storage - only stores refresh token in IndexedDB
536
959
  * Suitable for body-based refresh strategy
537
960
  * Persists refresh token for reuse after reload
961
+ *
962
+ * @param options - Storage options or legacy dbName string
963
+ * @param legacyRefreshTokenKey - Legacy refreshTokenKey (for backward compatibility)
538
964
  */
539
- declare function createIndexedDBStorage(dbName?: string, refreshTokenKey?: string): RefreshTokenStorage;
965
+ declare function createIndexedDBStorage(options?: IndexedDBStorageOptions | string, legacyRefreshTokenKey?: string): RefreshTokenStorage;
540
966
 
541
967
  /**
542
968
  * Body parser - parse token from response body (JSON)
@@ -638,15 +1064,18 @@ declare function createBodyProvider(config: {
638
1064
 
639
1065
  /**
640
1066
  * Serialize FormData for transfer over postMessage
641
- * Inspired by api-worker.js:484-518
642
1067
  *
643
- * FormData cannot be cloned via postMessage, so we need to serialize it first
644
- * Files are converted to ArrayBuffer -> number[] for transfer
1068
+ * FormData cannot be cloned via postMessage, so we need to serialize it first.
1069
+ * Files are converted to ArrayBuffer and returned as transferables for zero-copy transfer.
1070
+ *
1071
+ * IMPORTANT: Preserves original field order by using single-pass iteration.
1072
+ *
1073
+ * @returns SerializedFormDataResult with data and transferables array
645
1074
  */
646
- declare function serializeFormData(formData: FormData): Promise<SerializedFormData>;
1075
+ declare function serializeFormData(formData: FormData): Promise<SerializedFormDataResult>;
647
1076
  /**
648
1077
  * Deserialize SerializedFormData back to FormData in worker
649
- * Reconstructs File objects from serialized data
1078
+ * Reconstructs File objects from transferred ArrayBuffers
650
1079
  */
651
1080
  declare function deserializeFormData(serialized: SerializedFormData): FormData;
652
1081
  /**
@@ -669,4 +1098,138 @@ declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
669
1098
  */
670
1099
  declare function isBinaryContentType(contentType: string): boolean;
671
1100
 
672
- export { type ApiResponse, AuthErrors, type AuthResult, type AuthStrategy, DomainErrors, FetchGuardClient, type FetchGuardOptions, type FetchGuardRequestInit, GeneralErrors, InitErrors, MSG, type MainToWorkerMessage, type MessageType, type ProviderConfig, type ProviderPresetConfig, type RefreshTokenStorage, RequestErrors, type SerializedFile, type SerializedFormData, type SerializedFormDataEntry, type TokenInfo, type TokenParser, type TokenProvider, type WorkerConfig, type WorkerToMainMessage, base64ToArrayBuffer, bodyParser, bodyStrategy, clearProviders, cookieParser, cookieStrategy, createBodyProvider, createBodyStrategy, createClient, createCookieProvider, createCookieStrategy, createIndexedDBStorage, createProvider, deserializeFormData, getProvider, hasProvider, isBinaryContentType, isFormData, isSerializedFormData, listProviders, registerProvider, serializeFormData, unregisterProvider };
1101
+ /**
1102
+ * Helper functions for common Result patterns
1103
+ *
1104
+ * These utilities simplify working with FetchGuard's Result-based API
1105
+ * by providing type-safe helpers for common operations.
1106
+ */
1107
+
1108
+ /**
1109
+ * Check if result is a network/transport error (not an HTTP response)
1110
+ *
1111
+ * @example
1112
+ * const result = await api.fetch('/users')
1113
+ * if (isNetworkError(result)) {
1114
+ * console.error('Network failed:', result.errors[0]?.message)
1115
+ * }
1116
+ */
1117
+ declare function isNetworkError(result: Result<FetchEnvelope>): result is Result<FetchEnvelope> & {
1118
+ ok: false;
1119
+ };
1120
+ /**
1121
+ * Check if response is successful (2xx status)
1122
+ *
1123
+ * @example
1124
+ * if (isSuccess(result)) {
1125
+ * const data = parseJson(result)
1126
+ * }
1127
+ */
1128
+ declare function isSuccess(result: Result<FetchEnvelope>): boolean;
1129
+ /**
1130
+ * Check if response is a client error (4xx status)
1131
+ *
1132
+ * @example
1133
+ * if (isClientError(result)) {
1134
+ * console.error('Bad request:', getErrorMessage(result))
1135
+ * }
1136
+ */
1137
+ declare function isClientError(result: Result<FetchEnvelope>): boolean;
1138
+ /**
1139
+ * Check if response is a server error (5xx status)
1140
+ *
1141
+ * @example
1142
+ * if (isServerError(result)) {
1143
+ * // Maybe retry?
1144
+ * }
1145
+ */
1146
+ declare function isServerError(result: Result<FetchEnvelope>): boolean;
1147
+ /**
1148
+ * Parse JSON body safely with optional type inference
1149
+ *
1150
+ * Returns null if:
1151
+ * - Result is a network error (no response)
1152
+ * - Body is not valid JSON
1153
+ *
1154
+ * @example
1155
+ * const users = parseJson<User[]>(result)
1156
+ * if (users) {
1157
+ * // Use users array
1158
+ * }
1159
+ */
1160
+ declare function parseJson<T = unknown>(result: Result<FetchEnvelope>): T | null;
1161
+ /**
1162
+ * Get human-readable error message from result
1163
+ *
1164
+ * For network errors: returns the error message
1165
+ * For HTTP errors: tries to parse message from body, falls back to status code
1166
+ *
1167
+ * @example
1168
+ * if (!isSuccess(result)) {
1169
+ * toast.error(getErrorMessage(result))
1170
+ * }
1171
+ */
1172
+ declare function getErrorMessage(result: Result<FetchEnvelope>): string;
1173
+ /**
1174
+ * Get error body with type safety (best-effort parsing)
1175
+ *
1176
+ * NOTE: This is best-effort parsing. The error body comes from the server
1177
+ * and may not match the expected shape. Always handle null return and
1178
+ * validate before using typed properties.
1179
+ *
1180
+ * @example
1181
+ * interface ApiError {
1182
+ * code: string
1183
+ * message: string
1184
+ * errors?: { field: string; message: string }[]
1185
+ * }
1186
+ *
1187
+ * const errorBody = getErrorBody<ApiError>(result)
1188
+ * if (errorBody?.errors) {
1189
+ * errorBody.errors.forEach(e => console.error(`${e.field}: ${e.message}`))
1190
+ * }
1191
+ */
1192
+ declare function getErrorBody<T = unknown>(result: Result<FetchEnvelope>): T | null;
1193
+ /**
1194
+ * Get the HTTP status code from result
1195
+ *
1196
+ * Returns null if result is a network error (no HTTP response)
1197
+ *
1198
+ * @example
1199
+ * const status = getStatus(result)
1200
+ * if (status === 401) {
1201
+ * // Redirect to login
1202
+ * }
1203
+ */
1204
+ declare function getStatus(result: Result<FetchEnvelope>): number | null;
1205
+ /**
1206
+ * Check if result has a specific HTTP status
1207
+ *
1208
+ * @example
1209
+ * if (hasStatus(result, 404)) {
1210
+ * console.log('Not found')
1211
+ * }
1212
+ */
1213
+ declare function hasStatus(result: Result<FetchEnvelope>, status: number): boolean;
1214
+ /**
1215
+ * Match result against multiple handlers
1216
+ *
1217
+ * @example
1218
+ * matchResult(result, {
1219
+ * success: (data) => console.log('Success:', data),
1220
+ * clientError: (data) => console.error('Client error:', data.status),
1221
+ * serverError: (data) => console.error('Server error:', data.status),
1222
+ * networkError: (errors) => console.error('Network:', errors[0]?.message)
1223
+ * })
1224
+ */
1225
+ declare function matchResult<T>(result: Result<FetchEnvelope>, handlers: {
1226
+ success?: (data: FetchEnvelope) => T;
1227
+ clientError?: (data: FetchEnvelope) => T;
1228
+ serverError?: (data: FetchEnvelope) => T;
1229
+ networkError?: (errors: readonly {
1230
+ code: string;
1231
+ message: string;
1232
+ }[]) => T;
1233
+ }): T | undefined;
1234
+
1235
+ export { AuthErrors, type AuthResult, type AuthStrategy, type DebugHooks, type DedupeConfig, DomainErrors, ERROR_CODES, type ErrorCode, type ErrorCodeKey, type ExchangeTokenOptions, 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 };