@foundatiofx/fetchclient 1.1.0 → 1.2.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.
Files changed (45) hide show
  1. package/esm/mod.js +83 -0
  2. package/esm/src/DefaultHelpers.js +12 -5
  3. package/esm/src/FetchClient.js +66 -41
  4. package/esm/src/FetchClientError.js +73 -0
  5. package/esm/src/FetchClientProvider.js +24 -0
  6. package/esm/src/RateLimitMiddleware.js +35 -1
  7. package/esm/src/ResponsePromise.js +163 -0
  8. package/esm/src/RetryMiddleware.js +179 -0
  9. package/esm/src/mocks/MockHistory.js +8 -0
  10. package/esm/src/mocks/MockRegistry.js +7 -0
  11. package/package.json +1 -1
  12. package/readme.md +8 -15
  13. package/script/mod.js +94 -1
  14. package/script/src/DefaultHelpers.js +13 -5
  15. package/script/src/FetchClient.js +66 -41
  16. package/script/src/FetchClientError.js +77 -0
  17. package/script/src/FetchClientProvider.js +24 -0
  18. package/script/src/RateLimitMiddleware.js +36 -0
  19. package/script/src/ResponsePromise.js +167 -0
  20. package/script/src/RetryMiddleware.js +184 -0
  21. package/script/src/mocks/MockHistory.js +8 -0
  22. package/script/src/mocks/MockRegistry.js +7 -0
  23. package/types/mod.d.ts +96 -0
  24. package/types/mod.d.ts.map +1 -1
  25. package/types/src/DefaultHelpers.d.ts +11 -5
  26. package/types/src/DefaultHelpers.d.ts.map +1 -1
  27. package/types/src/FetchClient.d.ts +31 -15
  28. package/types/src/FetchClient.d.ts.map +1 -1
  29. package/types/src/FetchClientError.d.ts +29 -0
  30. package/types/src/FetchClientError.d.ts.map +1 -0
  31. package/types/src/FetchClientProvider.d.ts +11 -0
  32. package/types/src/FetchClientProvider.d.ts.map +1 -1
  33. package/types/src/RateLimitMiddleware.d.ts +27 -0
  34. package/types/src/RateLimitMiddleware.d.ts.map +1 -1
  35. package/types/src/ResponsePromise.d.ts +93 -0
  36. package/types/src/ResponsePromise.d.ts.map +1 -0
  37. package/types/src/RetryMiddleware.d.ts +88 -0
  38. package/types/src/RetryMiddleware.d.ts.map +1 -0
  39. package/types/src/mocks/MockHistory.d.ts +1 -0
  40. package/types/src/mocks/MockHistory.d.ts.map +1 -1
  41. package/types/src/mocks/MockRegistry.d.ts +5 -0
  42. package/types/src/mocks/MockRegistry.d.ts.map +1 -1
  43. package/types/src/mocks/types.d.ts +2 -0
  44. package/types/src/mocks/types.d.ts.map +1 -1
  45. package/types/src/tests/RetryMiddleware.test.d.ts.map +1 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * A promise that resolves to a FetchClientResponse with additional helper methods
3
+ * for parsing the response body. This allows for a fluent API similar to ky:
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // Await to get the full response
8
+ * const response = await client.get("/api/users");
9
+ *
10
+ * // Or use helper methods for direct access to parsed body
11
+ * const users = await client.get("/api/users").json<User[]>();
12
+ * const html = await client.get("/page").text();
13
+ * const file = await client.get("/file").blob();
14
+ * ```
15
+ */
16
+ export class ResponsePromise {
17
+ #responsePromise;
18
+ #options;
19
+ constructor(responsePromise, options) {
20
+ this.#responsePromise = responsePromise;
21
+ this.#options = options;
22
+ }
23
+ /**
24
+ * Implements PromiseLike interface so the ResponsePromise can be awaited.
25
+ */
26
+ then(onfulfilled, onrejected) {
27
+ return this.#responsePromise.then(onfulfilled, onrejected);
28
+ }
29
+ /**
30
+ * Catches any errors from the response promise.
31
+ */
32
+ catch(onrejected) {
33
+ return this.#responsePromise.catch(onrejected);
34
+ }
35
+ /**
36
+ * Executes a callback when the promise settles (fulfilled or rejected).
37
+ */
38
+ finally(onfinally) {
39
+ return this.#responsePromise.finally(onfinally);
40
+ }
41
+ /**
42
+ * Parses the response body as JSON.
43
+ *
44
+ * If the response was already parsed as JSON (via getJSON, postJSON, etc.),
45
+ * returns the parsed data directly. Otherwise, parses the response body.
46
+ *
47
+ * @template TJson - The expected type of the JSON response
48
+ * @returns A promise that resolves to the parsed JSON
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const user = await client.get("/api/user/1").json<User>();
53
+ * ```
54
+ */
55
+ async json() {
56
+ const response = await this.#responsePromise;
57
+ // If the response already has parsed data (from getJSON, etc.), return it
58
+ if (response.data !== null && response.data !== undefined) {
59
+ return response.data;
60
+ }
61
+ // Otherwise, parse the response body as JSON
62
+ const data = await response.json();
63
+ // Apply reviver and date parsing if options are set
64
+ if (this.#options?.reviver || this.#options?.shouldParseDates) {
65
+ return this.#reviveJson(data);
66
+ }
67
+ return data;
68
+ }
69
+ /**
70
+ * Returns the response body as text.
71
+ *
72
+ * @returns A promise that resolves to the response text
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const html = await client.get("/page").text();
77
+ * ```
78
+ */
79
+ async text() {
80
+ const response = await this.#responsePromise;
81
+ return response.text();
82
+ }
83
+ /**
84
+ * Returns the response body as a Blob.
85
+ *
86
+ * @returns A promise that resolves to the response as a Blob
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const imageBlob = await client.get("/image.png").blob();
91
+ * ```
92
+ */
93
+ async blob() {
94
+ const response = await this.#responsePromise;
95
+ return response.blob();
96
+ }
97
+ /**
98
+ * Returns the response body as an ArrayBuffer.
99
+ *
100
+ * @returns A promise that resolves to the response as an ArrayBuffer
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const buffer = await client.get("/file").arrayBuffer();
105
+ * ```
106
+ */
107
+ async arrayBuffer() {
108
+ const response = await this.#responsePromise;
109
+ return response.arrayBuffer();
110
+ }
111
+ /**
112
+ * Returns the response body as FormData.
113
+ *
114
+ * @returns A promise that resolves to the response as FormData
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const formData = await client.get("/form").formData();
119
+ * ```
120
+ */
121
+ async formData() {
122
+ const response = await this.#responsePromise;
123
+ return response.formData();
124
+ }
125
+ #reviveJson(data) {
126
+ if (data === null || data === undefined) {
127
+ return data;
128
+ }
129
+ if (Array.isArray(data)) {
130
+ return data.map((item) => this.#reviveJson(item));
131
+ }
132
+ if (typeof data === "object") {
133
+ const result = {};
134
+ for (const [key, value] of Object.entries(data)) {
135
+ result[key] = this.#reviveValue(key, this.#reviveJson(value));
136
+ }
137
+ return result;
138
+ }
139
+ return this.#reviveValue("", data);
140
+ }
141
+ #reviveValue(key, value) {
142
+ let revivedValue = value;
143
+ if (this.#options?.reviver) {
144
+ revivedValue = this.#options.reviver.call(this, key, revivedValue);
145
+ }
146
+ if (this.#options?.shouldParseDates) {
147
+ revivedValue = this.#tryParseDate(revivedValue);
148
+ }
149
+ return revivedValue;
150
+ }
151
+ #tryParseDate(value) {
152
+ if (typeof value !== "string") {
153
+ return value;
154
+ }
155
+ if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
156
+ const date = new Date(value);
157
+ if (!isNaN(date.getTime())) {
158
+ return date;
159
+ }
160
+ }
161
+ return value;
162
+ }
163
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Default HTTP methods that are eligible for retry.
3
+ * These are idempotent methods that can be safely retried without side effects.
4
+ */
5
+ const DEFAULT_RETRY_METHODS = [
6
+ "GET",
7
+ "HEAD",
8
+ "PUT",
9
+ "DELETE",
10
+ "OPTIONS",
11
+ "TRACE",
12
+ ];
13
+ /**
14
+ * Default HTTP status codes that trigger a retry.
15
+ */
16
+ const DEFAULT_RETRY_STATUS_CODES = [
17
+ 408, // Request Timeout
18
+ 413, // Payload Too Large (rate limiting)
19
+ 429, // Too Many Requests
20
+ 500, // Internal Server Error
21
+ 502, // Bad Gateway
22
+ 503, // Service Unavailable
23
+ 504, // Gateway Timeout
24
+ ];
25
+ /**
26
+ * Retry middleware that automatically retries failed requests with exponential backoff.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const provider = new FetchClientProvider();
31
+ * provider.useRetry({
32
+ * limit: 3,
33
+ * statusCodes: [500, 502, 503, 504],
34
+ * jitter: 0.1,
35
+ * });
36
+ *
37
+ * const client = provider.getFetchClient();
38
+ * const response = await client.getJSON('/api/data');
39
+ * ```
40
+ */
41
+ export class RetryMiddleware {
42
+ #options;
43
+ constructor(options) {
44
+ this.#options = {
45
+ limit: options?.limit ?? 2,
46
+ methods: (options?.methods ?? DEFAULT_RETRY_METHODS).map((m) => m.toUpperCase()),
47
+ statusCodes: options?.statusCodes ?? DEFAULT_RETRY_STATUS_CODES,
48
+ maxRetryAfter: options?.maxRetryAfter ?? Infinity,
49
+ backoffLimit: options?.backoffLimit ?? 30000,
50
+ jitter: options?.jitter ?? 0.1,
51
+ delay: options?.delay,
52
+ shouldRetry: options?.shouldRetry,
53
+ onRetry: options?.onRetry,
54
+ };
55
+ }
56
+ /**
57
+ * Creates the middleware function.
58
+ * @returns The middleware function
59
+ */
60
+ middleware() {
61
+ return async (context, next) => {
62
+ const method = context.request.method.toUpperCase();
63
+ // Check if method is eligible for retry
64
+ if (!this.#options.methods.includes(method)) {
65
+ await next();
66
+ return;
67
+ }
68
+ let attemptNumber = 0;
69
+ while (true) {
70
+ // Store retry metadata in context for observability
71
+ if (attemptNumber > 0) {
72
+ context.retryAttempt = attemptNumber;
73
+ }
74
+ await next();
75
+ // If no response or we've exhausted retries, stop
76
+ if (!context.response || attemptNumber >= this.#options.limit) {
77
+ break;
78
+ }
79
+ const response = context.response;
80
+ // Check if status code is retryable
81
+ if (!this.#options.statusCodes.includes(response.status)) {
82
+ break;
83
+ }
84
+ // Check custom shouldRetry predicate
85
+ if (this.#options.shouldRetry) {
86
+ const shouldRetry = await this.#options.shouldRetry(response, attemptNumber);
87
+ if (!shouldRetry) {
88
+ break;
89
+ }
90
+ }
91
+ // Calculate base delay
92
+ let delay = this.#calculateDelay(attemptNumber, response);
93
+ // Check Retry-After header
94
+ const retryAfterDelay = this.#parseRetryAfter(response);
95
+ if (retryAfterDelay !== null) {
96
+ // If Retry-After exceeds maxRetryAfter, don't retry
97
+ if (retryAfterDelay > this.#options.maxRetryAfter) {
98
+ break;
99
+ }
100
+ // Use the larger of computed delay or Retry-After
101
+ delay = Math.max(delay, retryAfterDelay);
102
+ }
103
+ // Invoke onRetry callback
104
+ this.#options.onRetry?.(attemptNumber, response, delay);
105
+ // Wait before retry
106
+ await this.#sleep(delay);
107
+ // Reset response for next attempt
108
+ context.response = null;
109
+ attemptNumber++;
110
+ }
111
+ };
112
+ }
113
+ /**
114
+ * Calculates the delay for a given attempt with exponential backoff and jitter.
115
+ */
116
+ #calculateDelay(attemptNumber, response) {
117
+ let baseDelay;
118
+ if (this.#options.delay) {
119
+ baseDelay = this.#options.delay(attemptNumber, response);
120
+ }
121
+ else {
122
+ // Default exponential backoff: 1s, 2s, 4s, 8s, ...
123
+ baseDelay = Math.min(1000 * Math.pow(2, attemptNumber), this.#options.backoffLimit);
124
+ }
125
+ // Apply jitter
126
+ return this.#applyJitter(baseDelay);
127
+ }
128
+ /**
129
+ * Applies jitter to a delay value.
130
+ */
131
+ #applyJitter(delay) {
132
+ if (this.#options.jitter <= 0) {
133
+ return delay;
134
+ }
135
+ const jitterRange = delay * this.#options.jitter;
136
+ // Random value between -jitterRange and +jitterRange
137
+ const jitter = (Math.random() * 2 - 1) * jitterRange;
138
+ return Math.max(0, Math.round(delay + jitter));
139
+ }
140
+ /**
141
+ * Parses the Retry-After header and returns the delay in milliseconds.
142
+ * Supports both delta-seconds and HTTP-date formats.
143
+ */
144
+ #parseRetryAfter(response) {
145
+ const retryAfter = response.headers.get("Retry-After");
146
+ if (!retryAfter) {
147
+ return null;
148
+ }
149
+ // Try parsing as seconds (integer)
150
+ const seconds = parseInt(retryAfter, 10);
151
+ if (!isNaN(seconds)) {
152
+ return seconds * 1000;
153
+ }
154
+ // Try parsing as HTTP-date
155
+ const date = Date.parse(retryAfter);
156
+ if (!isNaN(date)) {
157
+ return Math.max(0, date - Date.now());
158
+ }
159
+ return null;
160
+ }
161
+ /**
162
+ * Sleep for the specified number of milliseconds.
163
+ */
164
+ #sleep(ms) {
165
+ return new Promise((resolve) => setTimeout(resolve, ms));
166
+ }
167
+ }
168
+ /**
169
+ * Creates a retry middleware with the given options.
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * const client = new FetchClient();
174
+ * client.use(createRetryMiddleware({ limit: 3 }));
175
+ * ```
176
+ */
177
+ export function createRetryMiddleware(options) {
178
+ return new RetryMiddleware(options).middleware();
179
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export class MockHistoryImpl {
5
5
  #get = [];
6
+ #head = [];
6
7
  #post = [];
7
8
  #put = [];
8
9
  #patch = [];
@@ -11,6 +12,9 @@ export class MockHistoryImpl {
11
12
  get get() {
12
13
  return [...this.#get];
13
14
  }
15
+ get head() {
16
+ return [...this.#head];
17
+ }
14
18
  get post() {
15
19
  return [...this.#post];
16
20
  }
@@ -35,6 +39,9 @@ export class MockHistoryImpl {
35
39
  case "GET":
36
40
  this.#get.push(request);
37
41
  break;
42
+ case "HEAD":
43
+ this.#head.push(request);
44
+ break;
38
45
  case "POST":
39
46
  this.#post.push(request);
40
47
  break;
@@ -54,6 +61,7 @@ export class MockHistoryImpl {
54
61
  */
55
62
  clear() {
56
63
  this.#get = [];
64
+ this.#head = [];
57
65
  this.#post = [];
58
66
  this.#put = [];
59
67
  this.#patch = [];
@@ -40,6 +40,13 @@ export class MockRegistry {
40
40
  onGet(url) {
41
41
  return this.#addMock("GET", url);
42
42
  }
43
+ /**
44
+ * Creates a mock for HEAD requests matching the given URL.
45
+ * @param url - URL string or RegExp to match
46
+ */
47
+ onHead(url) {
48
+ return this.#addMock("HEAD", url);
49
+ }
43
50
  /**
44
51
  * Creates a mock for POST requests matching the given URL.
45
52
  * @param url - URL string or RegExp to match
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foundatiofx/fetchclient",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A typed JSON fetch client with middleware support for Deno, Node and the browser.",
5
5
  "keywords": [
6
6
  "Fetch",
package/readme.md CHANGED
@@ -14,7 +14,7 @@ handling.
14
14
 
15
15
  - **Typed JSON helpers** - `getJSON`, `postJSON`, `putJSON`, `patchJSON`,
16
16
  `deleteJSON`
17
- - **Two API styles** - Functional (no classes) or class-based - your choice
17
+ - **Two API styles** - Functional or class-based - your choice
18
18
  - **Response caching** - TTL-based caching with tags for grouped invalidation
19
19
  - **Middleware** - Intercept requests/responses for logging, auth, transforms
20
20
  - **Rate limiting** - Per-domain rate limits with automatic header detection
@@ -33,15 +33,13 @@ npm install @foundatiofx/fetchclient
33
33
 
34
34
  FetchClient works two ways - pick whichever style you prefer:
35
35
 
36
- ### Functional API (no classes)
36
+ ### Functional API
37
37
 
38
38
  ```ts
39
39
  import { getJSON, postJSON, setBaseUrl } from "@foundatiofx/fetchclient";
40
40
 
41
- // Optional: configure once at startup
42
41
  setBaseUrl("https://api.example.com");
43
42
 
44
- // Use simple functions anywhere
45
43
  const { data: users } = await getJSON<User[]>("/users");
46
44
  const { data: created } = await postJSON<User>("/users", { name: "Alice" });
47
45
  ```
@@ -69,9 +67,6 @@ const client = new FetchClient({ baseUrl: "https://api.example.com" });
69
67
  const { data } = await client.getJSON<User[]>("/users");
70
68
  ```
71
69
 
72
- All styles share the same configuration - the functional API wraps a
73
- [default provider](https://fetchclient.foundatio.dev/guide/provider#default-provider).
74
-
75
70
  ## Caching
76
71
 
77
72
  ```ts
@@ -112,12 +107,11 @@ usePerDomainRateLimit({
112
107
  ## Circuit Breaker
113
108
 
114
109
  ```ts
115
- import { FetchClientProvider } from "@foundatiofx/fetchclient";
110
+ import { useCircuitBreaker } from "@foundatiofx/fetchclient";
116
111
 
117
- const provider = new FetchClientProvider();
118
- provider.useCircuitBreaker({
119
- failureThreshold: 5, // Open after 5 failures
120
- openDurationMs: 30000, // Stay open for 30 seconds
112
+ useCircuitBreaker({
113
+ failureThreshold: 5,
114
+ openDurationMs: 30000,
121
115
  });
122
116
 
123
117
  // When API fails repeatedly, circuit opens
@@ -133,10 +127,9 @@ import { MockRegistry } from "@foundatiofx/fetchclient/mocks";
133
127
  const mocks = new MockRegistry();
134
128
  mocks.onGet("/api/users").reply(200, [{ id: 1, name: "Alice" }]);
135
129
 
136
- const provider = new FetchClientProvider();
137
- mocks.install(provider);
130
+ const client = new FetchClient();
131
+ mocks.install(client);
138
132
 
139
- const client = provider.getFetchClient();
140
133
  const { data } = await client.getJSON("/api/users");
141
134
  // data = [{ id: 1, name: "Alice" }]
142
135
  ```
package/script/mod.js CHANGED
@@ -14,9 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createPerDomainCircuitBreakerMiddleware = exports.createCircuitBreakerMiddleware = exports.CircuitOpenError = exports.CircuitBreakerMiddleware = exports.circuitBreakerGroupByDomain = exports.CircuitBreaker = exports.FetchClientProvider = exports.defaultProviderInstance = exports.FetchClientCache = exports.ProblemDetails = exports.FetchClient = void 0;
17
+ exports.middleware = exports.RateLimiter = exports.rateLimiterGroupByDomain = exports.RateLimitMiddleware = exports.RateLimitError = exports.createRateLimitMiddleware = exports.createPerDomainRateLimitMiddleware = exports.RetryMiddleware = exports.createRetryMiddleware = exports.createPerDomainCircuitBreakerMiddleware = exports.createCircuitBreakerMiddleware = exports.CircuitOpenError = exports.CircuitBreakerMiddleware = exports.circuitBreakerGroupByDomain = exports.CircuitBreaker = exports.FetchClientProvider = exports.defaultProviderInstance = exports.FetchClientCache = exports.ProblemDetails = exports.ResponsePromise = exports.FetchClientError = exports.FetchClient = void 0;
18
18
  var FetchClient_js_1 = require("./src/FetchClient.js");
19
19
  Object.defineProperty(exports, "FetchClient", { enumerable: true, get: function () { return FetchClient_js_1.FetchClient; } });
20
+ var FetchClientError_js_1 = require("./src/FetchClientError.js");
21
+ Object.defineProperty(exports, "FetchClientError", { enumerable: true, get: function () { return FetchClientError_js_1.FetchClientError; } });
22
+ var ResponsePromise_js_1 = require("./src/ResponsePromise.js");
23
+ Object.defineProperty(exports, "ResponsePromise", { enumerable: true, get: function () { return ResponsePromise_js_1.ResponsePromise; } });
20
24
  var ProblemDetails_js_1 = require("./src/ProblemDetails.js");
21
25
  Object.defineProperty(exports, "ProblemDetails", { enumerable: true, get: function () { return ProblemDetails_js_1.ProblemDetails; } });
22
26
  var FetchClientCache_js_1 = require("./src/FetchClientCache.js");
@@ -33,3 +37,92 @@ Object.defineProperty(exports, "CircuitBreakerMiddleware", { enumerable: true, g
33
37
  Object.defineProperty(exports, "CircuitOpenError", { enumerable: true, get: function () { return CircuitBreakerMiddleware_js_1.CircuitOpenError; } });
34
38
  Object.defineProperty(exports, "createCircuitBreakerMiddleware", { enumerable: true, get: function () { return CircuitBreakerMiddleware_js_1.createCircuitBreakerMiddleware; } });
35
39
  Object.defineProperty(exports, "createPerDomainCircuitBreakerMiddleware", { enumerable: true, get: function () { return CircuitBreakerMiddleware_js_1.createPerDomainCircuitBreakerMiddleware; } });
40
+ var RetryMiddleware_js_1 = require("./src/RetryMiddleware.js");
41
+ Object.defineProperty(exports, "createRetryMiddleware", { enumerable: true, get: function () { return RetryMiddleware_js_1.createRetryMiddleware; } });
42
+ Object.defineProperty(exports, "RetryMiddleware", { enumerable: true, get: function () { return RetryMiddleware_js_1.RetryMiddleware; } });
43
+ var RateLimitMiddleware_js_1 = require("./src/RateLimitMiddleware.js");
44
+ Object.defineProperty(exports, "createPerDomainRateLimitMiddleware", { enumerable: true, get: function () { return RateLimitMiddleware_js_1.createPerDomainRateLimitMiddleware; } });
45
+ Object.defineProperty(exports, "createRateLimitMiddleware", { enumerable: true, get: function () { return RateLimitMiddleware_js_1.createRateLimitMiddleware; } });
46
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return RateLimitMiddleware_js_1.RateLimitError; } });
47
+ Object.defineProperty(exports, "RateLimitMiddleware", { enumerable: true, get: function () { return RateLimitMiddleware_js_1.RateLimitMiddleware; } });
48
+ var RateLimiter_js_1 = require("./src/RateLimiter.js");
49
+ Object.defineProperty(exports, "rateLimiterGroupByDomain", { enumerable: true, get: function () { return RateLimiter_js_1.groupByDomain; } });
50
+ Object.defineProperty(exports, "RateLimiter", { enumerable: true, get: function () { return RateLimiter_js_1.RateLimiter; } });
51
+ const RetryMiddleware_js_2 = require("./src/RetryMiddleware.js");
52
+ const RateLimitMiddleware_js_2 = require("./src/RateLimitMiddleware.js");
53
+ const CircuitBreakerMiddleware_js_2 = require("./src/CircuitBreakerMiddleware.js");
54
+ const DefaultHelpers_js_1 = require("./src/DefaultHelpers.js");
55
+ /**
56
+ * Convenience middleware factory functions for use with FetchClient.use()
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { FetchClient, middleware } from "@foundatiofx/fetchclient";
61
+ *
62
+ * const client = new FetchClient();
63
+ * client.use(
64
+ * middleware.retry({ limit: 3 }),
65
+ * middleware.rateLimit({ maxRequests: 100, windowSeconds: 60 }),
66
+ * middleware.circuitBreaker({ failureThreshold: 5 })
67
+ * );
68
+ * ```
69
+ */
70
+ exports.middleware = {
71
+ /** Retry failed requests with exponential backoff and jitter */
72
+ retry: RetryMiddleware_js_2.createRetryMiddleware,
73
+ /** Rate limit requests to prevent overwhelming servers */
74
+ rateLimit: RateLimitMiddleware_js_2.createRateLimitMiddleware,
75
+ /** Per-domain rate limit (each domain tracked separately) */
76
+ perDomainRateLimit: RateLimitMiddleware_js_2.createPerDomainRateLimitMiddleware,
77
+ /** Circuit breaker for fault tolerance */
78
+ circuitBreaker: CircuitBreakerMiddleware_js_2.createCircuitBreakerMiddleware,
79
+ /** Per-domain circuit breaker (each domain tracked separately) */
80
+ perDomainCircuitBreaker: CircuitBreakerMiddleware_js_2.createPerDomainCircuitBreakerMiddleware,
81
+ };
82
+ /**
83
+ * Default export for convenient access to all HTTP methods.
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * import fc from "@foundatiofx/fetchclient";
88
+ *
89
+ * // Configure middleware
90
+ * fc.use(fc.middleware.retry({ limit: 3 }));
91
+ *
92
+ * // Use JSON methods (recommended)
93
+ * const { data: user } = await fc.getJSON<User>("/api/user/1");
94
+ * const { data: created } = await fc.postJSON<User>("/api/users", { name: "Alice" });
95
+ *
96
+ * // Or use fluent API for other response types
97
+ * const html = await fc.get("/page").text();
98
+ * ```
99
+ */
100
+ const fetchClient = {
101
+ /** Sends a GET request. Use `.json<T>()` for typed JSON response. */
102
+ get: (url, options) => (0, DefaultHelpers_js_1.useFetchClient)().get(url, options),
103
+ /** Sends a POST request. Use `.json<T>()` for typed JSON response. */
104
+ post: (url, body, options) => (0, DefaultHelpers_js_1.useFetchClient)().post(url, body, options),
105
+ /** Sends a PUT request. Use `.json<T>()` for typed JSON response. */
106
+ put: (url, body, options) => (0, DefaultHelpers_js_1.useFetchClient)().put(url, body, options),
107
+ /** Sends a PATCH request. Use `.json<T>()` for typed JSON response. */
108
+ patch: (url, body, options) => (0, DefaultHelpers_js_1.useFetchClient)().patch(url, body, options),
109
+ /** Sends a DELETE request. Use `.json<T>()` for typed JSON response. */
110
+ delete: (url, options) => (0, DefaultHelpers_js_1.useFetchClient)().delete(url, options),
111
+ /** Sends a HEAD request. */
112
+ head: (url, options) => (0, DefaultHelpers_js_1.useFetchClient)().head(url, options),
113
+ /** Sends a GET request and returns parsed JSON in response.data */
114
+ getJSON: DefaultHelpers_js_1.getJSON,
115
+ /** Sends a POST request and returns parsed JSON in response.data */
116
+ postJSON: DefaultHelpers_js_1.postJSON,
117
+ /** Sends a PUT request and returns parsed JSON in response.data */
118
+ putJSON: DefaultHelpers_js_1.putJSON,
119
+ /** Sends a PATCH request and returns parsed JSON in response.data */
120
+ patchJSON: DefaultHelpers_js_1.patchJSON,
121
+ /** Sends a DELETE request and returns parsed JSON in response.data */
122
+ deleteJSON: DefaultHelpers_js_1.deleteJSON,
123
+ /** Adds middleware to the default provider */
124
+ use: DefaultHelpers_js_1.useMiddleware,
125
+ /** Middleware factory functions */
126
+ middleware: exports.middleware,
127
+ };
128
+ exports.default = fetchClient;
@@ -7,6 +7,7 @@ exports.putJSON = putJSON;
7
7
  exports.patchJSON = patchJSON;
8
8
  exports.deleteJSON = deleteJSON;
9
9
  exports.getCurrentProvider = getCurrentProvider;
10
+ exports.getCache = getCache;
10
11
  exports.setCurrentProviderFunc = setCurrentProviderFunc;
11
12
  exports.setBaseUrl = setBaseUrl;
12
13
  exports.setAccessTokenFunc = setAccessTokenFunc;
@@ -30,7 +31,7 @@ function useFetchClient(options) {
30
31
  * Sends a GET request to the specified URL using the default client and provider and returns the response as JSON.
31
32
  * @param url - The URL to send the GET request to.
32
33
  * @param options - Optional request options.
33
- * @returns A promise that resolves to the response as JSON.
34
+ * @returns A promise that resolves to the response with parsed JSON in `data`.
34
35
  */
35
36
  function getJSON(url, options) {
36
37
  return useFetchClient().getJSON(url, options);
@@ -42,7 +43,7 @@ function getJSON(url, options) {
42
43
  * @param {string} url - The URL to send the request to.
43
44
  * @param {object | string | FormData} [body] - The JSON payload or form data to send with the request.
44
45
  * @param {RequestOptions} [options] - Additional options for the request.
45
- * @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
46
+ * @returns A promise that resolves to the response with parsed JSON in `data`.
46
47
  */
47
48
  function postJSON(url, body, options) {
48
49
  return useFetchClient().postJSON(url, body, options);
@@ -54,7 +55,7 @@ function postJSON(url, body, options) {
54
55
  * @param {string} url - The URL to send the request to.
55
56
  * @param {object | string} [body] - The JSON payload to send with the request.
56
57
  * @param {RequestOptions} [options] - Additional options for the request.
57
- * @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
58
+ * @returns A promise that resolves to the response with parsed JSON in `data`.
58
59
  */
59
60
  function putJSON(url, body, options) {
60
61
  return useFetchClient().putJSON(url, body, options);
@@ -66,7 +67,7 @@ function putJSON(url, body, options) {
66
67
  * @param {string} url - The URL to send the request to.
67
68
  * @param {object | string} [body] - The JSON payload to send with the request.
68
69
  * @param {RequestOptions} [options] - Additional options for the request.
69
- * @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
70
+ * @returns A promise that resolves to the response with parsed JSON in `data`.
70
71
  */
71
72
  function patchJSON(url, body, options) {
72
73
  return useFetchClient().patchJSON(url, body, options);
@@ -77,7 +78,7 @@ function patchJSON(url, body, options) {
77
78
  * @template T - The type of the response data.
78
79
  * @param {string} url - The URL to send the request to.
79
80
  * @param {RequestOptions} [options] - Additional options for the request.
80
- * @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
81
+ * @returns A promise that resolves to the response with parsed JSON in `data`.
81
82
  */
82
83
  function deleteJSON(url, options) {
83
84
  return useFetchClient().deleteJSON(url, options);
@@ -92,6 +93,13 @@ function getCurrentProvider() {
92
93
  }
93
94
  return getCurrentProviderFunc() ?? FetchClientProvider_js_1.defaultInstance;
94
95
  }
96
+ /**
97
+ * Gets the cache from the current provider.
98
+ * @returns The FetchClientCache instance.
99
+ */
100
+ function getCache() {
101
+ return getCurrentProvider().cache;
102
+ }
95
103
  /**
96
104
  * Sets the function that retrieves the current FetchClientProvider using whatever scoping mechanism is available.
97
105
  * @param getProviderFunc - The function that retrieves the current FetchClientProvider.