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/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
- * API response structure
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 ApiResponse {
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
- data: number[];
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 isProcessingQueue;
210
- private queueTimeout;
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<ApiResponse>>;
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<ApiResponse>>;
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<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>>;
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 sequentially
609
+ * Process message queue with concurrency limit
610
+ *
611
+ * Uses semaphore pattern to allow N concurrent requests.
311
612
  * Benefits:
312
- * - Sequential processing prevents worker overload
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: ApiResponse;
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: any;
460
- } & Partial<ts_micro_result_factories_errors_advanced.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
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: any;
470
- } & Partial<ts_micro_result_factories_errors_advanced.BaseErrorParams>) | undefined) => ts_micro_result.ErrorDetail;
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(dbName?: string, refreshTokenKey?: string): RefreshTokenStorage;
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 -> number[] for transfer
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<SerializedFormData>;
1021
+ declare function serializeFormData(formData: FormData): Promise<SerializedFormDataResult>;
647
1022
  /**
648
1023
  * Deserialize SerializedFormData back to FormData in worker
649
- * Reconstructs File objects from serialized data
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
- 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 };
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 };