@fgrzl/fetch 1.1.0-alpha.2 → 1.1.0-alpha.3

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 CHANGED
@@ -24,7 +24,12 @@ npm install @fgrzl/fetch
24
24
  ```ts
25
25
  import api from "@fgrzl/fetch";
26
26
 
27
- const data = await api.get("/api/user");
27
+ const response = await api.get("/api/user");
28
+ if (response.ok) {
29
+ console.log(response.data); // Your typed data
30
+ } else {
31
+ console.error(`Error ${response.status}:`, response.error?.message);
32
+ }
28
33
  ```
29
34
 
30
35
  Or create a custom instance:
@@ -44,1095 +49,28 @@ useCSRF(client, {
44
49
  useUnauthorized(client, {
45
50
  loginPath: "/login",
46
51
  });
47
- ```
48
-
49
- ## 🧩 Middleware
50
-
51
- ### Request Middleware
52
-
53
- Request middleware functions run before the HTTP request is sent, allowing you to modify request options and URLs.
54
-
55
- ```ts
56
- import { FetchClient, RequestMiddleware } from "@fgrzl/fetch";
57
-
58
- const client = new FetchClient();
59
-
60
- // Add authentication header
61
- client.useRequestMiddleware(async (req, url) => {
62
- const token = localStorage.getItem("auth-token");
63
- const headers = {
64
- ...req.headers,
65
- ...(token && { Authorization: `Bearer ${token}` }),
66
- };
67
- return [{ ...req, headers }, url];
68
- });
69
-
70
- // Add debug information
71
- client.useRequestMiddleware(async (req, url) => {
72
- const headers = {
73
- ...req.headers,
74
- "X-Debug": "true",
75
- "X-Timestamp": new Date().toISOString(),
76
- };
77
- return [{ ...req, headers }, url];
78
- });
79
- ```
80
-
81
- ### Response Middleware
82
-
83
- Response middleware functions run after the HTTP response is received, allowing you to process or modify responses.
84
-
85
- ```ts
86
- import { ResponseMiddleware } from "@fgrzl/fetch";
87
-
88
- // Log response times
89
- client.useResponseMiddleware(async (response) => {
90
- console.log(`Request to ${response.url} took ${performance.now()}ms`);
91
- return response;
92
- });
93
-
94
- // Extract and store updated auth tokens
95
- client.useResponseMiddleware(async (response) => {
96
- const newToken = response.headers.get("X-New-Auth-Token");
97
- if (newToken) {
98
- localStorage.setItem("auth-token", newToken);
99
- }
100
- return response;
101
- });
102
- ```
103
-
104
- ### Middleware Execution Order
105
-
106
- Middlewares execute in the order they are registered:
107
-
108
- 1. **Request middlewares**: Execute in registration order before the request
109
- 2. **Response middlewares**: Execute in registration order after the response
110
-
111
- ```ts
112
- const client = new FetchClient();
113
-
114
- // These will execute in this exact order:
115
- client.useRequestMiddleware(first); // 1st: runs first
116
- client.useRequestMiddleware(second); // 2nd: runs second
117
- client.useRequestMiddleware(third); // 3rd: runs third
118
-
119
- client.useResponseMiddleware(alpha); // 1st: processes response first
120
- client.useResponseMiddleware(beta); // 2nd: processes response second
121
- client.useResponseMiddleware(gamma); // 3rd: processes response third
122
- ```
123
-
124
- ## 🔄 Common Patterns
125
-
126
- ### Authentication with Token Retry
127
-
128
- Automatically retry requests with fresh tokens when authentication fails:
129
-
130
- ```ts
131
- import { FetchClient, HttpError } from "@fgrzl/fetch";
132
-
133
- const client = new FetchClient();
134
-
135
- // Request middleware: Add auth token
136
- client.useRequestMiddleware(async (req, url) => {
137
- const token = localStorage.getItem("auth-token");
138
- const headers = {
139
- ...req.headers,
140
- ...(token && { Authorization: `Bearer ${token}` }),
141
- };
142
- return [{ ...req, headers }, url];
143
- });
144
-
145
- // Response middleware: Handle token refresh
146
- client.useResponseMiddleware(async (response) => {
147
- if (response.status === 401) {
148
- // Try to refresh the token
149
- const refreshToken = localStorage.getItem("refresh-token");
150
- if (refreshToken) {
151
- try {
152
- const refreshResponse = await fetch("/auth/refresh", {
153
- method: "POST",
154
- headers: { Authorization: `Bearer ${refreshToken}` },
155
- });
156
-
157
- if (refreshResponse.ok) {
158
- const { access_token } = await refreshResponse.json();
159
- localStorage.setItem("auth-token", access_token);
160
-
161
- // Clone and retry the original request
162
- const retryResponse = await fetch(response.url, {
163
- ...response,
164
- headers: {
165
- ...response.headers,
166
- Authorization: `Bearer ${access_token}`,
167
- },
168
- });
169
- return retryResponse;
170
- }
171
- } catch (error) {
172
- console.error("Token refresh failed:", error);
173
- }
174
- }
175
-
176
- // Redirect to login if refresh fails
177
- window.location.href = "/login";
178
- }
179
- return response;
180
- });
181
- ```
182
-
183
- ### Request Correlation IDs
184
-
185
- Track requests across services with correlation IDs:
186
-
187
- ```ts
188
- import { v4 as uuidv4 } from "uuid";
189
-
190
- client.useRequestMiddleware(async (req, url) => {
191
- const correlationId = uuidv4();
192
-
193
- // Store correlation ID for debugging
194
- console.log(`Starting request ${correlationId} to ${url}`);
195
-
196
- const headers = {
197
- ...req.headers,
198
- "X-Correlation-ID": correlationId,
199
- "X-Request-ID": correlationId,
200
- };
201
-
202
- return [{ ...req, headers }, url];
203
- });
204
-
205
- client.useResponseMiddleware(async (response) => {
206
- const correlationId = response.headers.get("X-Correlation-ID");
207
- console.log(
208
- `Completed request ${correlationId} with status ${response.status}`,
209
- );
210
- return response;
211
- });
212
- ```
213
-
214
- ### Eventual Consistency Polling
215
-
216
- Handle read-after-write scenarios by polling until data is available:
217
-
218
- ```ts
219
- // Create a specialized client for polling operations
220
- const pollingClient = new FetchClient();
221
-
222
- pollingClient.useResponseMiddleware(async (response) => {
223
- // If we get 404 on a read after write, poll until available
224
- if (response.status === 404 && response.headers.get("X-Operation-ID")) {
225
- const operationId = response.headers.get("X-Operation-ID");
226
- const maxRetries = 10;
227
- const retryDelay = 1000; // 1 second
228
-
229
- for (let attempt = 0; attempt < maxRetries; attempt++) {
230
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
231
-
232
- try {
233
- const retryResponse = await fetch(response.url, {
234
- method: "GET",
235
- headers: { "X-Operation-ID": operationId },
236
- });
237
-
238
- if (retryResponse.ok) {
239
- return retryResponse;
240
- }
241
-
242
- if (retryResponse.status !== 404) {
243
- return retryResponse; // Return other errors immediately
244
- }
245
- } catch (error) {
246
- console.warn(`Polling attempt ${attempt + 1} failed:`, error);
247
- }
248
- }
249
- }
250
-
251
- return response;
252
- });
253
-
254
- // Usage for read-after-write operations
255
- const createUser = async (userData: any) => {
256
- // Write operation
257
- const createResponse = await client.post("/api/users", userData);
258
- const operationId = createResponse.headers.get("X-Operation-ID");
259
-
260
- // Read operation with polling fallback
261
- const userResponse = await pollingClient.get(
262
- `/api/users/${createResponse.id}`,
263
- {
264
- headers: { "X-Operation-ID": operationId },
265
- },
266
- );
267
-
268
- return userResponse;
269
- };
270
- ```
271
-
272
- ### Centralized Error Mapping
273
-
274
- Transform backend errors into user-friendly messages:
275
-
276
- ```ts
277
- // Define error mappings
278
- const errorMappings = {
279
- 400: "Invalid request. Please check your input.",
280
- 401: "Please log in to continue.",
281
- 403: "You don't have permission to perform this action.",
282
- 404: "The requested resource was not found.",
283
- 422: "Validation failed. Please check your input.",
284
- 429: "Too many requests. Please try again later.",
285
- 500: "An internal error occurred. Please try again.",
286
- 502: "Service temporarily unavailable.",
287
- 503: "Service temporarily unavailable.",
288
- 504: "Request timed out. Please try again.",
289
- };
290
52
 
291
- client.useResponseMiddleware(async (response) => {
292
- if (!response.ok) {
293
- const body = await response.json().catch(() => ({}));
294
-
295
- // Create user-friendly error with mapped message
296
- const userMessage =
297
- errorMappings[response.status] || "An unexpected error occurred.";
298
-
299
- // Add user-friendly message to error body
300
- const enhancedBody = {
301
- ...body,
302
- userMessage,
303
- originalStatus: response.status,
304
- timestamp: new Date().toISOString(),
305
- };
306
-
307
- // Create a new response with enhanced error information
308
- return new Response(JSON.stringify(enhancedBody), {
309
- status: response.status,
310
- statusText: response.statusText,
311
- headers: response.headers,
312
- });
313
- }
314
-
315
- return response;
316
- });
317
- ```
318
-
319
- ## 🔧 TypeScript Best Practices
320
-
321
- ### Typing Request and Response Shapes
322
-
323
- Define clear interfaces for your API contracts:
324
-
325
- ```ts
326
- import { FetchClient } from "@fgrzl/fetch";
327
-
328
- // Define API response types
329
- interface User {
330
- id: number;
331
- name: string;
332
- email: string;
333
- createdAt: string;
334
- }
335
-
336
- interface CreateUserRequest {
337
- name: string;
338
- email: string;
339
- }
340
-
341
- interface ApiResponse<T> {
342
- data: T;
343
- message: string;
344
- timestamp: string;
345
- }
346
-
347
- const client = new FetchClient();
348
-
349
- // Type-safe API calls
350
- const getUser = (id: number): Promise<ApiResponse<User>> => {
351
- return client.get<ApiResponse<User>>(`/api/users/${id}`);
352
- };
353
-
354
- const createUser = (
355
- userData: CreateUserRequest,
356
- ): Promise<ApiResponse<User>> => {
357
- return client.post<ApiResponse<User>>("/api/users", userData);
358
- };
359
-
360
- // Usage with full type safety
361
- const user = await getUser(123);
362
- console.log(user.data.name); // TypeScript knows this is a string
363
- ```
364
-
365
- ### Generic Middleware Patterns
366
-
367
- Create reusable, type-safe middleware:
368
-
369
- ```ts
370
- import { RequestMiddleware, ResponseMiddleware } from "@fgrzl/fetch";
371
-
372
- // Type-safe request middleware factory
373
- function createAuthMiddleware<T extends string>(
374
- tokenProvider: () => T | null,
375
- ): RequestMiddleware {
376
- return async (req, url) => {
377
- const token = tokenProvider();
378
- const headers = {
379
- ...req.headers,
380
- ...(token && { Authorization: `Bearer ${token}` }),
381
- };
382
- return [{ ...req, headers }, url];
383
- };
384
- }
385
-
386
- // Type-safe response middleware for data transformation
387
- function createDataTransformMiddleware<TInput, TOutput>(
388
- transformer: (input: TInput) => TOutput,
389
- ): ResponseMiddleware {
390
- return async (response) => {
391
- if (response.ok && response.headers.get("content-type")?.includes("json")) {
392
- const data = (await response.json()) as TInput;
393
- const transformedData = transformer(data);
394
-
395
- return new Response(JSON.stringify(transformedData), {
396
- status: response.status,
397
- statusText: response.statusText,
398
- headers: response.headers,
399
- });
400
- }
401
- return response;
402
- };
403
- }
404
-
405
- // Usage
406
- const authMiddleware = createAuthMiddleware(() =>
407
- localStorage.getItem("token"),
408
- );
409
- const transformMiddleware = createDataTransformMiddleware<
410
- RawApiData,
411
- CleanData
412
- >((raw) => ({ ...raw, processedAt: new Date() }));
413
-
414
- client.useRequestMiddleware(authMiddleware);
415
- client.useResponseMiddleware(transformMiddleware);
416
- ```
417
-
418
- ### Type-Safe Error Handling
419
-
420
- Create typed error handlers for different scenarios:
421
-
422
- ```ts
423
- import { HttpError, NetworkError, FetchError } from "@fgrzl/fetch";
424
-
425
- // Define error types for your API
426
- interface ApiError {
427
- code: string;
428
- message: string;
429
- details?: Record<string, any>;
430
- }
431
-
432
- interface ValidationError extends ApiError {
433
- code: "VALIDATION_ERROR";
434
- details: {
435
- field: string;
436
- message: string;
437
- }[];
438
- }
439
-
440
- // Type-safe error handling utility
441
- async function handleApiCall<T>(
442
- apiCall: () => Promise<T>,
443
- ): Promise<{ data?: T; error?: string }> {
444
- try {
445
- const data = await apiCall();
446
- return { data };
447
- } catch (error) {
448
- if (error instanceof HttpError) {
449
- const apiError = error.body as ApiError;
450
-
451
- switch (apiError.code) {
452
- case "VALIDATION_ERROR":
453
- const validationError = apiError as ValidationError;
454
- return {
455
- error: `Validation failed: ${validationError.details.map((d) => d.message).join(", ")}`,
456
- };
457
-
458
- case "UNAUTHORIZED":
459
- return { error: "Please log in to continue" };
460
-
461
- default:
462
- return { error: apiError.message || "An error occurred" };
463
- }
464
- }
465
-
466
- if (error instanceof NetworkError) {
467
- return { error: "Network error. Please check your connection." };
468
- }
469
-
470
- return { error: "An unexpected error occurred" };
471
- }
472
- }
473
-
474
- // Usage
475
- const result = await handleApiCall(() => client.get<User>("/api/users/123"));
476
- if (result.error) {
477
- console.error(result.error);
53
+ // All requests now return FetchResponse<T>
54
+ interface User { id: number; name: string; }
55
+ const userResponse = await client.get<User>("/api/user");
56
+ if (userResponse.ok) {
57
+ console.log(userResponse.data.name); // Typed access to data
478
58
  } else {
479
- console.log(result.data.name); // TypeScript knows data is User
480
- }
481
- ```
482
-
483
- ## 🚀 Advanced Usage
484
-
485
- ### Conditional Middleware Application
486
-
487
- Apply middleware only for specific routes or conditions:
488
-
489
- ```ts
490
- import { RequestMiddleware, ResponseMiddleware } from "@fgrzl/fetch";
491
-
492
- // Conditional request middleware
493
- const conditionalAuthMiddleware: RequestMiddleware = async (req, url) => {
494
- // Only add auth to protected routes
495
- if (url.includes("/api/protected/") || url.includes("/api/admin/")) {
496
- const token = localStorage.getItem("admin-token");
497
- const headers = {
498
- ...req.headers,
499
- ...(token && { Authorization: `Bearer ${token}` }),
500
- };
501
- return [{ ...req, headers }, url];
502
- }
503
- return [req, url];
504
- };
505
-
506
- // Conditional response middleware
507
- const conditionalCachingMiddleware: ResponseMiddleware = async (response) => {
508
- // Only cache GET requests to specific endpoints
509
- if (response.url.includes("/api/cache/") && response.status === 200) {
510
- const cacheKey = `cache_${response.url}`;
511
- const data = await response.clone().text();
512
- localStorage.setItem(cacheKey, data);
513
- localStorage.setItem(`${cacheKey}_timestamp`, Date.now().toString());
514
- }
515
- return response;
516
- };
517
-
518
- client.useRequestMiddleware(conditionalAuthMiddleware);
519
- client.useResponseMiddleware(conditionalCachingMiddleware);
520
- ```
521
-
522
- ### Middleware Composition and Factories
523
-
524
- Create composable middleware for complex scenarios:
525
-
526
- ```ts
527
- // Middleware factory for different environments
528
- function createEnvironmentMiddleware(environment: "dev" | "staging" | "prod") {
529
- const configs = {
530
- dev: { baseUrl: "http://localhost:3000", debug: true },
531
- staging: { baseUrl: "https://staging-api.example.com", debug: true },
532
- prod: { baseUrl: "https://api.example.com", debug: false },
533
- };
534
-
535
- const config = configs[environment];
536
-
537
- const requestMiddleware: RequestMiddleware = async (req, url) => {
538
- // Convert relative URLs to absolute
539
- const fullUrl = url.startsWith("/") ? `${config.baseUrl}${url}` : url;
540
-
541
- const headers = {
542
- ...req.headers,
543
- "X-Environment": environment,
544
- ...(config.debug && { "X-Debug": "true" }),
545
- };
546
-
547
- return [{ ...req, headers }, fullUrl];
548
- };
549
-
550
- const responseMiddleware: ResponseMiddleware = async (response) => {
551
- if (config.debug) {
552
- console.log(
553
- `[${environment.upper()}] ${response.status} ${response.url}`,
554
- );
555
- }
556
- return response;
557
- };
558
-
559
- return { requestMiddleware, responseMiddleware };
560
- }
561
-
562
- // Middleware composition utility
563
- function composeMiddleware(
564
- ...middlewares: RequestMiddleware[]
565
- ): RequestMiddleware {
566
- return async (req, url) => {
567
- let currentReq = req;
568
- let currentUrl = url;
569
-
570
- for (const middleware of middlewares) {
571
- [currentReq, currentUrl] = await middleware(currentReq, currentUrl);
572
- }
573
-
574
- return [currentReq, currentUrl];
575
- };
59
+ console.error(`Failed with status ${userResponse.status}`);
576
60
  }
577
-
578
- // Usage
579
- const { requestMiddleware, responseMiddleware } =
580
- createEnvironmentMiddleware("dev");
581
-
582
- const composedMiddleware = composeMiddleware(
583
- requestMiddleware,
584
- createAuthMiddleware(() => localStorage.getItem("token")),
585
- createLoggingMiddleware(),
586
- );
587
-
588
- client.useRequestMiddleware(composedMiddleware);
589
- client.useResponseMiddleware(responseMiddleware);
590
- ```
591
-
592
- ### Performance Optimizations
593
-
594
- Optimize middleware for high-throughput applications:
595
-
596
- ```ts
597
- // Cached middleware to avoid repeated computations
598
- const createCachedAuthMiddleware = (): RequestMiddleware => {
599
- let cachedToken: string | null = null;
600
- let tokenExpiry: number = 0;
601
-
602
- return async (req, url) => {
603
- const now = Date.now();
604
-
605
- // Refresh token only if expired
606
- if (!cachedToken || now > tokenExpiry) {
607
- cachedToken = localStorage.getItem("auth-token");
608
- // Cache for 5 minutes
609
- tokenExpiry = now + 5 * 60 * 1000;
610
- }
611
-
612
- const headers = {
613
- ...req.headers,
614
- ...(cachedToken && { Authorization: `Bearer ${cachedToken}` }),
615
- };
616
-
617
- return [{ ...req, headers }, url];
618
- };
619
- };
620
-
621
- // Debounced middleware for rate limiting
622
- const createDebouncedMiddleware = (delay: number = 100): RequestMiddleware => {
623
- const pending = new Map<string, Promise<[RequestInit, string]>>();
624
-
625
- return async (req, url) => {
626
- const key = `${req.method || "GET"}:${url}`;
627
-
628
- if (pending.has(key)) {
629
- return pending.get(key)!;
630
- }
631
-
632
- const promise = new Promise<[RequestInit, string]>((resolve) => {
633
- setTimeout(() => {
634
- pending.delete(key);
635
- resolve([req, url]);
636
- }, delay);
637
- });
638
-
639
- pending.set(key, promise);
640
- return promise;
641
- };
642
- };
643
-
644
- // Circuit breaker pattern
645
- const createCircuitBreakerMiddleware = (
646
- failureThreshold: number = 5,
647
- resetTimeout: number = 60000,
648
- ): ResponseMiddleware => {
649
- let failures = 0;
650
- let lastFailureTime = 0;
651
- let isOpen = false;
652
-
653
- return async (response) => {
654
- const now = Date.now();
655
-
656
- // Reset circuit if timeout has passed
657
- if (isOpen && now - lastFailureTime > resetTimeout) {
658
- isOpen = false;
659
- failures = 0;
660
- }
661
-
662
- if (isOpen) {
663
- throw new Error("Circuit breaker is open");
664
- }
665
-
666
- if (!response.ok && response.status >= 500) {
667
- failures++;
668
- lastFailureTime = now;
669
-
670
- if (failures >= failureThreshold) {
671
- isOpen = true;
672
- console.warn("Circuit breaker opened due to repeated failures");
673
- }
674
- } else if (response.ok) {
675
- // Reset on success
676
- failures = 0;
677
- }
678
-
679
- return response;
680
- };
681
- };
682
- ```
683
-
684
- ### Complete Integration Example
685
-
686
- Here's a complete example showing multiple patterns working together:
687
-
688
- ```ts
689
- import { FetchClient, HttpError } from "@fgrzl/fetch";
690
-
691
- // Types
692
- interface ApiConfig {
693
- baseUrl: string;
694
- environment: "dev" | "staging" | "prod";
695
- enableRetry: boolean;
696
- enableCircuitBreaker: boolean;
697
- }
698
-
699
- interface User {
700
- id: number;
701
- name: string;
702
- email: string;
703
- }
704
-
705
- // Create configured client
706
- function createApiClient(config: ApiConfig): FetchClient {
707
- const client = new FetchClient({
708
- credentials: "same-origin",
709
- });
710
-
711
- // Environment-specific middleware
712
- client.useRequestMiddleware(async (req, url) => {
713
- const fullUrl = url.startsWith("/") ? `${config.baseUrl}${url}` : url;
714
- const headers = {
715
- ...req.headers,
716
- "Content-Type": "application/json",
717
- "X-Environment": config.environment,
718
- "X-Client-Version": "1.0.0",
719
- };
720
- return [{ ...req, headers }, fullUrl];
721
- });
722
-
723
- // Auth middleware
724
- client.useRequestMiddleware(createCachedAuthMiddleware());
725
-
726
- // Correlation ID middleware
727
- client.useRequestMiddleware(async (req, url) => {
728
- const correlationId = crypto.randomUUID();
729
- const headers = {
730
- ...req.headers,
731
- "X-Correlation-ID": correlationId,
732
- };
733
- return [{ ...req, headers }, url];
734
- });
735
-
736
- // Circuit breaker (production only)
737
- if (config.enableCircuitBreaker && config.environment === "prod") {
738
- client.useResponseMiddleware(createCircuitBreakerMiddleware());
739
- }
740
-
741
- // Retry middleware
742
- if (config.enableRetry) {
743
- client.useResponseMiddleware(async (response) => {
744
- if (response.status >= 500 && response.status < 600) {
745
- // Retry logic here
746
- console.log("Retrying request due to server error...");
747
- }
748
- return response;
749
- });
750
- }
751
-
752
- // Error mapping
753
- client.useResponseMiddleware(async (response) => {
754
- if (!response.ok) {
755
- const correlationId = response.headers.get("X-Correlation-ID");
756
- console.error(`Request failed [${correlationId}]:`, response.status);
757
- }
758
- return response;
759
- });
760
-
761
- return client;
762
- }
763
-
764
- // Usage
765
- const apiClient = createApiClient({
766
- baseUrl: "https://api.example.com",
767
- environment: "prod",
768
- enableRetry: true,
769
- enableCircuitBreaker: true,
770
- });
771
-
772
- // Type-safe API methods
773
- const userApi = {
774
- getUser: (id: number): Promise<User> => apiClient.get<User>(`/users/${id}`),
775
-
776
- createUser: (userData: Omit<User, "id">): Promise<User> =>
777
- apiClient.post<User>("/users", userData),
778
-
779
- updateUser: (id: number, userData: Partial<User>): Promise<User> =>
780
- apiClient.put<User>(`/users/${id}`, userData),
781
-
782
- deleteUser: (id: number): Promise<void> =>
783
- apiClient.del<void>(`/users/${id}`),
784
- };
785
-
786
- // Usage with error handling
787
- try {
788
- const user = await userApi.getUser(123);
789
- console.log("User loaded:", user.name);
790
- } catch (error) {
791
- if (error instanceof HttpError) {
792
- console.error("API Error:", error.status, error.body);
793
- } else {
794
- console.error("Unexpected error:", error);
795
- }
796
- }
797
- ```
798
-
799
- ## ⚡ Performance Considerations
800
-
801
- ### Middleware Order Optimization
802
-
803
- Order middleware strategically for best performance:
804
-
805
- ```ts
806
- const client = new FetchClient();
807
-
808
- // ✅ Fast middleware first (simple header additions)
809
- client.useRequestMiddleware(addCorrelationId);
810
- client.useRequestMiddleware(addTimestamp);
811
-
812
- // ✅ Medium complexity middleware
813
- client.useRequestMiddleware(addAuthToken);
814
- client.useRequestMiddleware(transformUrl);
815
-
816
- // ✅ Heavy middleware last (async operations, storage access)
817
- client.useRequestMiddleware(checkCacheAndModifyRequest);
818
- client.useRequestMiddleware(validateAndEnrichRequest);
819
-
820
- // Same principle for response middleware
821
- client.useResponseMiddleware(logResponse); // Fast
822
- client.useResponseMiddleware(extractHeaders); // Fast
823
- client.useResponseMiddleware(updateCache); // Heavy
824
- client.useResponseMiddleware(processComplexData); // Heavy
825
61
  ```
826
62
 
827
- ### Memory Management
828
-
829
- Avoid memory leaks in long-running applications:
830
-
831
- ```ts
832
- // ❌ Bad: Creates closures that hold references
833
- function badMiddlewareFactory() {
834
- const largeData = new Array(1000000).fill("data");
835
-
836
- return async (req, url) => {
837
- // This holds reference to largeData forever
838
- return [{ ...req, someData: largeData[0] }, url];
839
- };
840
- }
841
-
842
- // ✅ Good: Clean references and use weak references where appropriate
843
- function goodMiddlewareFactory() {
844
- return async (req, url) => {
845
- // Create data only when needed
846
- const necessaryData = computeNecessaryData();
847
- return [{ ...req, data: necessaryData }, url];
848
- };
849
- }
63
+ ---
850
64
 
851
- // ✅ Good: Use WeakMap for temporary caching
852
- const responseCache = new WeakMap<Response, any>();
65
+ ## Documentation
853
66
 
854
- const cachingMiddleware: ResponseMiddleware = async (response) => {
855
- if (!responseCache.has(response)) {
856
- const data = await response.clone().json();
857
- responseCache.set(response, data);
858
- }
859
- return response;
860
- };
861
- ```
862
-
863
- ### Request Batching and Caching
864
-
865
- Implement intelligent caching to reduce network requests:
866
-
867
- ```ts
868
- // Simple request deduplication
869
- class RequestDeduplicator {
870
- private pending = new Map<string, Promise<Response>>();
871
-
872
- createMiddleware(): RequestMiddleware {
873
- return async (req, url) => {
874
- const key = `${req.method || "GET"}:${url}:${JSON.stringify(req.body)}`;
875
-
876
- // For GET requests, deduplicate concurrent identical requests
877
- if (req.method === "GET" && this.pending.has(key)) {
878
- console.log("Deduplicating request:", key);
879
- // Return same promise for identical concurrent requests
880
- await this.pending.get(key);
881
- }
882
-
883
- return [req, url];
884
- };
885
- }
886
- }
67
+ - [Project Overview](docs/overview.md)
68
+ - [Middleware](docs/middleware.md)
69
+ - [Error Handling](docs/errors.md)
70
+ - [Testing](docs/testing.md)
887
71
 
888
- const deduplicator = new RequestDeduplicator();
889
- client.useRequestMiddleware(deduplicator.createMiddleware());
72
+ ---
890
73
 
891
- // Response caching with TTL
892
- class ResponseCache {
893
- private cache = new Map<string, { data: any; expiry: number }>();
74
+ ## License
894
75
 
895
- createMiddleware(ttlMs: number = 300000): ResponseMiddleware {
896
- return async (response) => {
897
- if (response.ok && response.url.includes("/api/cache/")) {
898
- const key = response.url;
899
- const now = Date.now();
900
-
901
- // Clean expired entries
902
- for (const [k, v] of this.cache.entries()) {
903
- if (v.expiry < now) {
904
- this.cache.delete(k);
905
- }
906
- }
907
-
908
- // Cache successful responses
909
- const data = await response.clone().json();
910
- this.cache.set(key, { data, expiry: now + ttlMs });
911
- }
912
-
913
- return response;
914
- };
915
- }
916
- }
917
-
918
- const cache = new ResponseCache();
919
- client.useResponseMiddleware(cache.createMiddleware(5 * 60 * 1000)); // 5 minute TTL
920
- ```
921
-
922
- ### Monitoring and Metrics
923
-
924
- Track performance metrics for optimization:
925
-
926
- ```ts
927
- class PerformanceMonitor {
928
- private metrics = {
929
- requestCount: 0,
930
- responseCount: 0,
931
- averageResponseTime: 0,
932
- errorRate: 0,
933
- slowRequests: 0,
934
- };
935
-
936
- createRequestMiddleware(): RequestMiddleware {
937
- return async (req, url) => {
938
- this.metrics.requestCount++;
939
-
940
- // Add performance marker
941
- const startTime = performance.now();
942
- const headers = {
943
- ...req.headers,
944
- "X-Start-Time": startTime.toString(),
945
- };
946
-
947
- return [{ ...req, headers }, url];
948
- };
949
- }
950
-
951
- createResponseMiddleware(): ResponseMiddleware {
952
- return async (response) => {
953
- this.metrics.responseCount++;
954
-
955
- const startTime = parseFloat(response.headers.get("X-Start-Time") || "0");
956
- if (startTime > 0) {
957
- const responseTime = performance.now() - startTime;
958
-
959
- // Update average response time
960
- this.metrics.averageResponseTime =
961
- (this.metrics.averageResponseTime + responseTime) / 2;
962
-
963
- // Track slow requests (>2s)
964
- if (responseTime > 2000) {
965
- this.metrics.slowRequests++;
966
- console.warn(
967
- `Slow request detected: ${response.url} took ${responseTime}ms`,
968
- );
969
- }
970
- }
971
-
972
- // Track error rate
973
- if (!response.ok) {
974
- this.metrics.errorRate =
975
- (this.metrics.errorRate * (this.metrics.responseCount - 1) + 1) /
976
- this.metrics.responseCount;
977
- }
978
-
979
- return response;
980
- };
981
- }
982
-
983
- getMetrics() {
984
- return { ...this.metrics };
985
- }
986
-
987
- reset() {
988
- this.metrics = {
989
- requestCount: 0,
990
- responseCount: 0,
991
- averageResponseTime: 0,
992
- errorRate: 0,
993
- slowRequests: 0,
994
- };
995
- }
996
- }
997
-
998
- // Usage
999
- const monitor = new PerformanceMonitor();
1000
- client.useRequestMiddleware(monitor.createRequestMiddleware());
1001
- client.useResponseMiddleware(monitor.createResponseMiddleware());
1002
-
1003
- // Check metrics periodically
1004
- setInterval(() => {
1005
- const metrics = monitor.getMetrics();
1006
- console.log("API Performance:", metrics);
1007
-
1008
- if (metrics.errorRate > 0.1) {
1009
- // >10% error rate
1010
- console.warn("High error rate detected!", metrics);
1011
- }
1012
- }, 30000); // Every 30 seconds
1013
- ```
1014
-
1015
- ## 🔐 CSRF + 401 Handling
1016
-
1017
- The default export is pre-configured with:
1018
-
1019
- - `credentials: 'same-origin'`
1020
- - CSRF token from `csrf_token` cookie
1021
- - 401 redirect to `/login?returnTo=...`
1022
-
1023
- ## 📋 Quick Copy-Paste Examples
1024
-
1025
- ### Basic Auth Token
1026
-
1027
- ```ts
1028
- import { FetchClient } from "@fgrzl/fetch";
1029
-
1030
- const client = new FetchClient();
1031
- client.useRequestMiddleware(async (req, url) => {
1032
- const token = localStorage.getItem("token");
1033
- return [
1034
- {
1035
- ...req,
1036
- headers: { ...req.headers, Authorization: `Bearer ${token}` },
1037
- },
1038
- url,
1039
- ];
1040
- });
1041
-
1042
- // Usage
1043
- const data = await client.get("/api/protected-resource");
1044
- ```
1045
-
1046
- ### Request Logging
1047
-
1048
- ```ts
1049
- client.useRequestMiddleware(async (req, url) => {
1050
- console.log(`🚀 ${req.method || "GET"} ${url}`);
1051
- return [req, url];
1052
- });
1053
-
1054
- client.useResponseMiddleware(async (res) => {
1055
- console.log(`✅ ${res.status} ${res.url}`);
1056
- return res;
1057
- });
1058
- ```
1059
-
1060
- ### Automatic Retry
1061
-
1062
- ```ts
1063
- client.useResponseMiddleware(async (response) => {
1064
- if (response.status >= 500 && response.status < 600) {
1065
- console.log("Retrying request...");
1066
- await new Promise((resolve) => setTimeout(resolve, 1000));
1067
- return fetch(response.url, response);
1068
- }
1069
- return response;
1070
- });
1071
- ```
1072
-
1073
- ### Error Notifications
1074
-
1075
- ```ts
1076
- client.useResponseMiddleware(async (response) => {
1077
- if (!response.ok) {
1078
- const message = `Request failed: ${response.status} ${response.statusText}`;
1079
- // Show toast notification, update UI, etc.
1080
- console.error(message);
1081
- }
1082
- return response;
1083
- });
1084
- ```
1085
-
1086
- ### Development Debug Headers
1087
-
1088
- ```ts
1089
- if (process.env.NODE_ENV === "development") {
1090
- client.useRequestMiddleware(async (req, url) => {
1091
- return [
1092
- {
1093
- ...req,
1094
- headers: {
1095
- ...req.headers,
1096
- "X-Debug": "true",
1097
- "X-Timestamp": new Date().toISOString(),
1098
- "X-User-Agent": navigator.userAgent,
1099
- },
1100
- },
1101
- url,
1102
- ];
1103
- });
1104
- }
1105
- ```
1106
-
1107
- ### Simple Rate Limiting
1108
-
1109
- ```ts
1110
- let lastRequest = 0;
1111
- const RATE_LIMIT_MS = 1000; // 1 request per second
1112
-
1113
- client.useRequestMiddleware(async (req, url) => {
1114
- const now = Date.now();
1115
- const timeSinceLastRequest = now - lastRequest;
1116
-
1117
- if (timeSinceLastRequest < RATE_LIMIT_MS) {
1118
- await new Promise((resolve) =>
1119
- setTimeout(resolve, RATE_LIMIT_MS - timeSinceLastRequest),
1120
- );
1121
- }
1122
-
1123
- lastRequest = Date.now();
1124
- return [req, url];
1125
- });
1126
- ```
1127
-
1128
- ## 🧪 Testing
1129
-
1130
- ```bash
1131
- npm run test
1132
- ```
1133
-
1134
- ## 🛠 Build
1135
-
1136
- ```bash
1137
- npm run build
1138
- ```
76
+ MIT
package/dist/client.d.ts CHANGED
@@ -13,6 +13,29 @@ export type RequestMiddleware = (req: RequestInit, url: string) => Promise<[Requ
13
13
  * @returns A promise that resolves to the modified response
14
14
  */
15
15
  export type ResponseMiddleware = (res: Response) => Promise<Response>;
16
+ /**
17
+ * Typed response wrapper that includes response metadata.
18
+ * @template T - The type of the response data
19
+ */
20
+ export interface FetchResponse<T> {
21
+ /** The parsed response data */
22
+ data: T;
23
+ /** HTTP status code */
24
+ status: number;
25
+ /** HTTP status text */
26
+ statusText: string;
27
+ /** Response headers */
28
+ headers: Headers;
29
+ /** The original URL */
30
+ url: string;
31
+ /** Whether the response was successful (status 200-299) */
32
+ ok: boolean;
33
+ /** Error information if the request failed */
34
+ error?: {
35
+ message: string;
36
+ body?: any;
37
+ };
38
+ }
16
39
  /**
17
40
  * Configuration options for FetchClient.
18
41
  */
@@ -65,24 +88,23 @@ export declare class FetchClient {
65
88
  * @template T - The expected response type
66
89
  * @param url - The URL to request
67
90
  * @param init - Request configuration options
68
- * @returns Promise that resolves to the parsed JSON response
69
- * @throws The error object from the response if the request fails
91
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
70
92
  */
71
- request<T = any>(url: string, init?: RequestInit): Promise<T>;
93
+ request<T = any>(url: string, init?: RequestInit): Promise<FetchResponse<T>>;
72
94
  /**
73
95
  * Makes a GET request.
74
96
  * @template T - The expected response type
75
97
  * @param url - The URL to request
76
- * @returns Promise that resolves to the parsed JSON response
98
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
77
99
  */
78
- get<T>(url: string): Promise<T>;
100
+ get<T>(url: string): Promise<FetchResponse<T>>;
79
101
  /**
80
102
  * Helper method for requests with JSON body.
81
103
  * @template T - The expected response type
82
104
  * @param url - The URL to request
83
105
  * @param method - The HTTP method
84
106
  * @param body - The request body data (will be JSON stringified)
85
- * @returns Promise that resolves to the parsed JSON response
107
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
86
108
  */
87
109
  private requestWithJsonBody;
88
110
  /**
@@ -90,22 +112,22 @@ export declare class FetchClient {
90
112
  * @template T - The expected response type
91
113
  * @param url - The URL to request
92
114
  * @param body - The request body data (will be JSON stringified)
93
- * @returns Promise that resolves to the parsed JSON response
115
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
94
116
  */
95
- post<T>(url: string, body?: any): Promise<T>;
117
+ post<T>(url: string, body?: any): Promise<FetchResponse<T>>;
96
118
  /**
97
119
  * Makes a PUT request with JSON body.
98
120
  * @template T - The expected response type
99
121
  * @param url - The URL to request
100
122
  * @param body - The request body data (will be JSON stringified)
101
- * @returns Promise that resolves to the parsed JSON response
123
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
102
124
  */
103
- put<T>(url: string, body?: any): Promise<T>;
125
+ put<T>(url: string, body?: any): Promise<FetchResponse<T>>;
104
126
  /**
105
127
  * Makes a DELETE request.
106
128
  * @template T - The expected response type
107
129
  * @param url - The URL to request
108
- * @returns Promise that resolves to the parsed JSON response
130
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
109
131
  */
110
- del<T>(url: string): Promise<T>;
132
+ del<T>(url: string): Promise<FetchResponse<T>>;
111
133
  }
package/dist/client.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FetchClient = void 0;
4
- const errors_1 = require("./errors");
5
4
  /**
6
5
  * A configurable HTTP client with middleware support.
7
6
  *
@@ -52,8 +51,7 @@ class FetchClient {
52
51
  * @template T - The expected response type
53
52
  * @param url - The URL to request
54
53
  * @param init - Request configuration options
55
- * @returns Promise that resolves to the parsed JSON response
56
- * @throws The error object from the response if the request fails
54
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
57
55
  */
58
56
  async request(url, init = {}) {
59
57
  try {
@@ -67,19 +65,47 @@ class FetchClient {
67
65
  for (const mw of this.responseMiddlewares) {
68
66
  res = await mw(res);
69
67
  }
68
+ const responseMetadata = {
69
+ status: res.status,
70
+ statusText: res.statusText,
71
+ headers: res.headers,
72
+ url: res.url,
73
+ ok: res.ok,
74
+ };
70
75
  if (!res.ok) {
71
76
  const body = await res.json().catch(() => ({}));
72
- throw new errors_1.HttpError(res.status, res.statusText, body, url);
77
+ return {
78
+ ...responseMetadata,
79
+ data: null,
80
+ error: {
81
+ message: res.statusText,
82
+ body,
83
+ },
84
+ };
73
85
  }
74
- return res.json();
86
+ const data = await res.json();
87
+ return {
88
+ ...responseMetadata,
89
+ data,
90
+ };
75
91
  }
76
92
  catch (error) {
77
- if (error instanceof errors_1.HttpError) {
78
- throw error;
79
- }
93
+ // Handle network errors and other exceptions
80
94
  if (error instanceof TypeError && error.message.includes('fetch')) {
81
- throw new errors_1.NetworkError('Failed to fetch', url, error);
95
+ return {
96
+ status: 0,
97
+ statusText: 'Network Error',
98
+ headers: new Headers(),
99
+ url,
100
+ ok: false,
101
+ data: null,
102
+ error: {
103
+ message: 'Failed to fetch',
104
+ body: error,
105
+ },
106
+ };
82
107
  }
108
+ // Re-throw unexpected errors
83
109
  throw error;
84
110
  }
85
111
  }
@@ -87,7 +113,7 @@ class FetchClient {
87
113
  * Makes a GET request.
88
114
  * @template T - The expected response type
89
115
  * @param url - The URL to request
90
- * @returns Promise that resolves to the parsed JSON response
116
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
91
117
  */
92
118
  get(url) {
93
119
  return this.request(url, { method: 'GET' });
@@ -98,7 +124,7 @@ class FetchClient {
98
124
  * @param url - The URL to request
99
125
  * @param method - The HTTP method
100
126
  * @param body - The request body data (will be JSON stringified)
101
- * @returns Promise that resolves to the parsed JSON response
127
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
102
128
  */
103
129
  requestWithJsonBody(url, method, body) {
104
130
  return this.request(url, {
@@ -111,7 +137,7 @@ class FetchClient {
111
137
  * @template T - The expected response type
112
138
  * @param url - The URL to request
113
139
  * @param body - The request body data (will be JSON stringified)
114
- * @returns Promise that resolves to the parsed JSON response
140
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
115
141
  */
116
142
  post(url, body) {
117
143
  return this.requestWithJsonBody(url, 'POST', body ?? {});
@@ -121,7 +147,7 @@ class FetchClient {
121
147
  * @template T - The expected response type
122
148
  * @param url - The URL to request
123
149
  * @param body - The request body data (will be JSON stringified)
124
- * @returns Promise that resolves to the parsed JSON response
150
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
125
151
  */
126
152
  put(url, body) {
127
153
  return this.requestWithJsonBody(url, 'PUT', body ?? {});
@@ -130,7 +156,7 @@ class FetchClient {
130
156
  * Makes a DELETE request.
131
157
  * @template T - The expected response type
132
158
  * @param url - The URL to request
133
- * @returns Promise that resolves to the parsed JSON response
159
+ * @returns Promise that resolves to a FetchResponse containing the data and metadata
134
160
  */
135
161
  del(url) {
136
162
  return this.request(url, { method: 'DELETE' });
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,qCAAmD;AA8BnD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,WAAW;IAKtB;;;OAGG;IACH,YAAY,SAA4B,EAAE;QARlC,uBAAkB,GAAwB,EAAE,CAAC;QAC7C,wBAAmB,GAAyB,EAAE,CAAC;QAQrD,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,aAAa,CAAC;IACzD,CAAC;IAED;;;;OAIG;IACI,oBAAoB,CAAC,UAA6B;QACvD,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACI,qBAAqB,CAAC,UAA8B;QACzD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK,CAAC,OAAO,CAClB,GAAW,EACX,OAAoB,EAAE;QAEtB,IAAI,CAAC;YACH,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBACzC,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACpC,CAAC;YAED,IAAI,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBACzB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,GAAG,IAAI;aACR,CAAC,CAAC;YAEH,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBAC1C,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;YACtB,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAChD,MAAM,IAAI,kBAAS,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;YAC7D,CAAC;YAED,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,kBAAS,EAAE,CAAC;gBAC/B,MAAM,KAAK,CAAC;YACd,CAAC;YACD,IAAI,KAAK,YAAY,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAClE,MAAM,IAAI,qBAAY,CAAC,iBAAiB,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YACxD,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACI,GAAG,CAAI,GAAW;QACvB,OAAO,IAAI,CAAC,OAAO,CAAI,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;OAOG;IACK,mBAAmB,CACzB,GAAW,EACX,MAAsB,EACtB,IAAS;QAET,OAAO,IAAI,CAAC,OAAO,CAAI,GAAG,EAAE;YAC1B,MAAM;YACN,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,IAAI,CAAI,GAAW,EAAE,IAAU;QACpC,OAAO,IAAI,CAAC,mBAAmB,CAAI,GAAG,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;OAMG;IACI,GAAG,CAAI,GAAW,EAAE,IAAU;QACnC,OAAO,IAAI,CAAC,mBAAmB,CAAI,GAAG,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;OAKG;IACI,GAAG,CAAI,GAAW;QACvB,OAAO,IAAI,CAAC,OAAO,CAAI,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IACpD,CAAC;CACF;AAvID,kCAuIC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ declare const api: FetchClient;
30
30
  export default api;
31
31
  export { FetchError, HttpError, NetworkError } from './errors';
32
32
  export { FetchClient } from './client';
33
- export type { RequestMiddleware, ResponseMiddleware, FetchClientConfig, } from './client';
33
+ export type { RequestMiddleware, ResponseMiddleware, FetchClientConfig, FetchResponse, } from './client';
34
34
  export { useCSRF } from './csrf';
35
35
  export { useUnauthorized } from './unauthorized';
36
36
  export type { UnauthorizedConfig } from './unauthorized';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,qCAAuC;AACvC,iCAAiC;AACjC,iDAAiD;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,GAAG,GAAG,IAAI,oBAAW,CAAC;IAC1B,WAAW,EAAE,aAAa;CAC3B,CAAC,CAAC;AAEH,4BAA4B;AAC5B,IAAA,cAAO,EAAC,GAAG,EAAE;IACX,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,cAAc;CAC3B,CAAC,CAAC;AAEH,kCAAkC;AAClC,IAAA,8BAAe,EAAC,GAAG,EAAE;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC;AAEnB,+CAA+C;AAC/C,mCAA+D;AAAtD,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAC5C,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AAMpB,+BAAiC;AAAxB,+FAAA,OAAO,OAAA;AAChB,+CAAiD;AAAxC,+GAAA,eAAe,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,qCAAuC;AACvC,iCAAiC;AACjC,iDAAiD;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,GAAG,GAAG,IAAI,oBAAW,CAAC;IAC1B,WAAW,EAAE,aAAa;CAC3B,CAAC,CAAC;AAEH,4BAA4B;AAC5B,IAAA,cAAO,EAAC,GAAG,EAAE;IACX,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,cAAc;CAC3B,CAAC,CAAC;AAEH,kCAAkC;AAClC,IAAA,8BAAe,EAAC,GAAG,EAAE;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC;AAEnB,+CAA+C;AAC/C,mCAA+D;AAAtD,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAC5C,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AAOpB,+BAAiC;AAAxB,+FAAA,OAAO,OAAA;AAChB,+CAAiD;AAAxC,+GAAA,eAAe,OAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fgrzl/fetch",
3
- "version": "1.1.0-alpha.2",
4
- "description": "A simple fetch client",
3
+ "version": "1.1.0-alpha.3",
4
+ "description": "A simple fetch client with some extra goodies",
5
5
  "keywords": [
6
6
  "fetch",
7
7
  "typescript",
@@ -28,7 +28,7 @@
28
28
  "prepublishOnly": "npm run build",
29
29
  "format": "prettier --write .",
30
30
  "format:check": "prettier --check .",
31
- "test": "vitest",
31
+ "test": "vitest run",
32
32
  "test:watch": "vitest --watch",
33
33
  "test:coverage": "vitest run --coverage",
34
34
  "lint": "npm run format:check"