@foundatiofx/fetchclient 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/esm/mod.js +3 -1
  2. package/esm/src/CircuitBreaker.js +356 -0
  3. package/esm/src/CircuitBreakerMiddleware.js +167 -0
  4. package/esm/src/DefaultHelpers.js +16 -0
  5. package/esm/src/FetchClient.js +1 -1
  6. package/esm/src/FetchClientCache.js +85 -8
  7. package/esm/src/FetchClientProvider.js +58 -3
  8. package/esm/src/mocks/MockHistory.js +63 -0
  9. package/esm/src/mocks/MockRegistry.js +267 -0
  10. package/esm/src/mocks/MockResponseBuilder.js +88 -0
  11. package/esm/src/mocks/mod.js +24 -0
  12. package/esm/src/mocks/types.js +1 -0
  13. package/package.json +12 -2
  14. package/readme.md +125 -8
  15. package/script/mod.js +9 -1
  16. package/script/src/CircuitBreaker.js +361 -0
  17. package/script/src/CircuitBreakerMiddleware.js +174 -0
  18. package/script/src/DefaultHelpers.js +18 -0
  19. package/script/src/FetchClient.js +1 -1
  20. package/script/src/FetchClientCache.js +85 -8
  21. package/script/src/FetchClientProvider.js +58 -3
  22. package/script/src/mocks/MockHistory.js +67 -0
  23. package/script/src/mocks/MockRegistry.js +271 -0
  24. package/script/src/mocks/MockResponseBuilder.js +92 -0
  25. package/script/src/mocks/mod.js +29 -0
  26. package/script/src/mocks/types.js +2 -0
  27. package/types/deps/jsr.io/@std/assert/1.0.18/almost_equals.d.ts.map +1 -0
  28. package/types/deps/jsr.io/@std/assert/1.0.18/array_includes.d.ts.map +1 -0
  29. package/types/deps/jsr.io/@std/assert/1.0.18/assert.d.ts.map +1 -0
  30. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/assertion_error.d.ts.map +1 -1
  31. package/types/deps/jsr.io/@std/assert/1.0.18/equal.d.ts.map +1 -0
  32. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/equals.d.ts.map +1 -1
  33. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/exists.d.ts.map +1 -1
  34. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/fail.d.ts.map +1 -1
  35. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/false.d.ts.map +1 -1
  36. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/greater.d.ts.map +1 -1
  37. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/greater_or_equal.d.ts.map +1 -1
  38. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/instance_of.d.ts.map +1 -1
  39. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/is_error.d.ts.map +1 -1
  40. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/less.d.ts.map +1 -1
  41. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/less_or_equal.d.ts.map +1 -1
  42. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/match.d.ts.map +1 -1
  43. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/mod.d.ts.map +1 -1
  44. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/not_equals.d.ts.map +1 -1
  45. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/not_instance_of.d.ts.map +1 -1
  46. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/not_match.d.ts.map +1 -1
  47. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/not_strict_equals.d.ts.map +1 -1
  48. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/object_match.d.ts.map +1 -1
  49. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/rejects.d.ts.map +1 -1
  50. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/strict_equals.d.ts.map +1 -1
  51. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/string_includes.d.ts.map +1 -1
  52. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/throws.d.ts.map +1 -1
  53. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/unimplemented.d.ts.map +1 -1
  54. package/types/deps/jsr.io/@std/assert/{1.0.14 → 1.0.18}/unreachable.d.ts.map +1 -1
  55. package/types/deps/jsr.io/@std/internal/1.0.12/build_message.d.ts.map +1 -0
  56. package/types/deps/jsr.io/@std/internal/{1.0.10 → 1.0.12}/diff.d.ts.map +1 -1
  57. package/types/deps/jsr.io/@std/internal/1.0.12/diff_str.d.ts.map +1 -0
  58. package/types/deps/jsr.io/@std/internal/{1.0.10 → 1.0.12}/format.d.ts.map +1 -1
  59. package/types/deps/jsr.io/@std/internal/{1.0.10 → 1.0.12}/styles.d.ts.map +1 -1
  60. package/types/deps/jsr.io/@std/internal/1.0.12/types.d.ts.map +1 -0
  61. package/types/mod.d.ts +3 -1
  62. package/types/mod.d.ts.map +1 -1
  63. package/types/src/CircuitBreaker.d.ts +154 -0
  64. package/types/src/CircuitBreaker.d.ts.map +1 -0
  65. package/types/src/CircuitBreakerMiddleware.d.ts +93 -0
  66. package/types/src/CircuitBreakerMiddleware.d.ts.map +1 -0
  67. package/types/src/DefaultHelpers.d.ts +13 -0
  68. package/types/src/DefaultHelpers.d.ts.map +1 -1
  69. package/types/src/FetchClient.d.ts.map +1 -1
  70. package/types/src/FetchClientCache.d.ts +26 -1
  71. package/types/src/FetchClientCache.d.ts.map +1 -1
  72. package/types/src/FetchClientProvider.d.ts +24 -0
  73. package/types/src/FetchClientProvider.d.ts.map +1 -1
  74. package/types/src/RequestOptions.d.ts +6 -1
  75. package/types/src/RequestOptions.d.ts.map +1 -1
  76. package/types/src/mocks/MockHistory.d.ts +22 -0
  77. package/types/src/mocks/MockHistory.d.ts.map +1 -0
  78. package/types/src/mocks/MockRegistry.d.ts +113 -0
  79. package/types/src/mocks/MockRegistry.d.ts.map +1 -0
  80. package/types/src/mocks/MockResponseBuilder.d.ts +60 -0
  81. package/types/src/mocks/MockResponseBuilder.d.ts.map +1 -0
  82. package/types/src/mocks/mod.d.ts +26 -0
  83. package/types/src/mocks/mod.d.ts.map +1 -0
  84. package/types/src/mocks/types.d.ts +47 -0
  85. package/types/src/mocks/types.d.ts.map +1 -0
  86. package/types/src/tests/Caching.test.d.ts.map +1 -0
  87. package/types/src/tests/CircuitBreaker.test.d.ts.map +1 -0
  88. package/types/src/tests/ErrorHandling.test.d.ts.map +1 -0
  89. package/types/src/tests/HttpMethods.test.d.ts.map +1 -0
  90. package/types/src/tests/Integration.test.d.ts.map +1 -0
  91. package/types/src/tests/JsonParsing.test.d.ts.map +1 -0
  92. package/types/src/tests/Middleware.test.d.ts.map +1 -0
  93. package/types/src/tests/MockRegistry.test.d.ts.map +1 -0
  94. package/types/src/tests/Provider.test.d.ts.map +1 -0
  95. package/types/src/tests/RateLimit.test.d.ts.map +1 -0
  96. package/types/src/tests/TimeoutAbort.test.d.ts.map +1 -0
  97. package/types/src/tests/UrlBuilding.test.d.ts.map +1 -0
  98. package/types/deps/jsr.io/@std/assert/1.0.14/almost_equals.d.ts.map +0 -1
  99. package/types/deps/jsr.io/@std/assert/1.0.14/array_includes.d.ts.map +0 -1
  100. package/types/deps/jsr.io/@std/assert/1.0.14/assert.d.ts.map +0 -1
  101. package/types/deps/jsr.io/@std/assert/1.0.14/equal.d.ts.map +0 -1
  102. package/types/deps/jsr.io/@std/internal/1.0.10/build_message.d.ts.map +0 -1
  103. package/types/deps/jsr.io/@std/internal/1.0.10/diff_str.d.ts.map +0 -1
  104. package/types/deps/jsr.io/@std/internal/1.0.10/types.d.ts.map +0 -1
  105. package/types/src/FetchClient.test.d.ts.map +0 -1
  106. package/types/src/RateLimit.test.d.ts.map +0 -1
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CircuitBreakerMiddleware = exports.CircuitOpenError = void 0;
4
+ exports.createCircuitBreakerMiddleware = createCircuitBreakerMiddleware;
5
+ exports.createPerDomainCircuitBreakerMiddleware = createPerDomainCircuitBreakerMiddleware;
6
+ const CircuitBreaker_js_1 = require("./CircuitBreaker.js");
7
+ const ProblemDetails_js_1 = require("./ProblemDetails.js");
8
+ /**
9
+ * Error thrown when a request is blocked due to an open circuit.
10
+ */
11
+ class CircuitOpenError extends Error {
12
+ /** The group whose circuit is open */
13
+ group;
14
+ /** The current circuit state */
15
+ state;
16
+ /** When the circuit was opened (timestamp) */
17
+ openedAt;
18
+ /** Suggested retry time in seconds */
19
+ retryAfter;
20
+ constructor(group, state, openedAt, retryAfter, message) {
21
+ super(message ?? `Circuit breaker is open for ${group}`);
22
+ this.name = "CircuitOpenError";
23
+ this.group = group;
24
+ this.state = state;
25
+ this.openedAt = openedAt;
26
+ this.retryAfter = retryAfter;
27
+ }
28
+ }
29
+ exports.CircuitOpenError = CircuitOpenError;
30
+ /**
31
+ * Default function to determine if a response is a failure.
32
+ * Returns true for 5xx server errors and 429 rate limit responses.
33
+ */
34
+ function defaultIsFailure(response) {
35
+ return response.status >= 500 || response.status === 429;
36
+ }
37
+ /**
38
+ * Middleware that implements the circuit breaker pattern.
39
+ *
40
+ * When a service starts failing (5xx errors, timeouts, network errors),
41
+ * the circuit breaker opens and blocks further requests for a period,
42
+ * returning 503 Service Unavailable immediately without hitting the API.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const middleware = new CircuitBreakerMiddleware({
47
+ * failureThreshold: 5,
48
+ * openDurationMs: 30000,
49
+ * });
50
+ *
51
+ * provider.useMiddleware(middleware.middleware());
52
+ * ```
53
+ */
54
+ class CircuitBreakerMiddleware {
55
+ #circuitBreaker;
56
+ #throwOnOpen;
57
+ #errorMessage;
58
+ #isFailure;
59
+ #getGroupFunc;
60
+ #openDurationMs;
61
+ constructor(options) {
62
+ this.#circuitBreaker = new CircuitBreaker_js_1.CircuitBreaker(options);
63
+ this.#throwOnOpen = options?.throwOnOpen ?? false;
64
+ this.#errorMessage = options?.errorMessage;
65
+ this.#isFailure = options?.isFailure ?? defaultIsFailure;
66
+ this.#getGroupFunc = options?.getGroupFunc ?? (() => "global");
67
+ this.#openDurationMs = options?.openDurationMs ?? 30000;
68
+ }
69
+ /**
70
+ * Gets the underlying circuit breaker instance.
71
+ */
72
+ get circuitBreaker() {
73
+ return this.#circuitBreaker;
74
+ }
75
+ /**
76
+ * Creates the middleware function for use with FetchClient.
77
+ *
78
+ * @returns The middleware function
79
+ */
80
+ middleware() {
81
+ return async (ctx, next) => {
82
+ const url = ctx.request.url;
83
+ const group = this.#getGroupFunc(url);
84
+ // PRE-REQUEST: Check if circuit allows the request
85
+ if (!this.#circuitBreaker.isAllowed(url)) {
86
+ const timeSinceOpen = this.#circuitBreaker.getTimeSinceOpen(url) ?? 0;
87
+ const retryAfterMs = Math.max(0, this.#openDurationMs - timeSinceOpen);
88
+ const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
89
+ if (this.#throwOnOpen) {
90
+ throw new CircuitOpenError(group, this.#circuitBreaker.getState(url), Date.now() - timeSinceOpen, retryAfterSeconds, this.#errorMessage);
91
+ }
92
+ // Return synthetic 503 response
93
+ const problem = new ProblemDetails_js_1.ProblemDetails();
94
+ problem.status = 503;
95
+ problem.title = "Service Unavailable";
96
+ problem.detail = this.#errorMessage ??
97
+ `Circuit breaker is open for ${group}. Service may be experiencing issues.`;
98
+ const headers = new Headers({
99
+ "Content-Type": "application/problem+json",
100
+ "Retry-After": String(retryAfterSeconds),
101
+ });
102
+ const response = new Response(JSON.stringify(problem), {
103
+ status: 503,
104
+ statusText: "Service Unavailable",
105
+ headers,
106
+ });
107
+ // Attach problem details like FetchClient does
108
+ Object.assign(response, { problem, data: null });
109
+ ctx.response = response;
110
+ return;
111
+ }
112
+ // EXECUTE REQUEST
113
+ let isNetworkError = false;
114
+ try {
115
+ await next();
116
+ }
117
+ catch (error) {
118
+ // Network errors count as failures
119
+ isNetworkError = true;
120
+ this.#circuitBreaker.recordFailure(url);
121
+ throw error;
122
+ }
123
+ // POST-RESPONSE: Record result
124
+ if (!isNetworkError && ctx.response) {
125
+ if (this.#isFailure(ctx.response)) {
126
+ this.#circuitBreaker.recordFailure(url);
127
+ }
128
+ else {
129
+ this.#circuitBreaker.recordSuccess(url);
130
+ }
131
+ }
132
+ };
133
+ }
134
+ }
135
+ exports.CircuitBreakerMiddleware = CircuitBreakerMiddleware;
136
+ /**
137
+ * Creates a circuit breaker middleware with the given options.
138
+ *
139
+ * @param options - Circuit breaker configuration
140
+ * @returns The middleware function
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * provider.useMiddleware(createCircuitBreakerMiddleware({
145
+ * failureThreshold: 5,
146
+ * openDurationMs: 30000,
147
+ * }));
148
+ * ```
149
+ */
150
+ function createCircuitBreakerMiddleware(options) {
151
+ const middleware = new CircuitBreakerMiddleware(options);
152
+ return middleware.middleware();
153
+ }
154
+ /**
155
+ * Creates a per-domain circuit breaker middleware.
156
+ * Each domain gets its own circuit breaker.
157
+ *
158
+ * @param options - Circuit breaker configuration
159
+ * @returns The middleware function
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * provider.useMiddleware(createPerDomainCircuitBreakerMiddleware({
164
+ * failureThreshold: 5,
165
+ * openDurationMs: 30000,
166
+ * }));
167
+ * ```
168
+ */
169
+ function createPerDomainCircuitBreakerMiddleware(options) {
170
+ return createCircuitBreakerMiddleware({
171
+ ...options,
172
+ getGroupFunc: CircuitBreaker_js_1.groupByDomain,
173
+ });
174
+ }
@@ -15,6 +15,8 @@ exports.useMiddleware = useMiddleware;
15
15
  exports.setRequestOptions = setRequestOptions;
16
16
  exports.useRateLimit = useRateLimit;
17
17
  exports.usePerDomainRateLimit = usePerDomainRateLimit;
18
+ exports.useCircuitBreaker = useCircuitBreaker;
19
+ exports.usePerDomainCircuitBreaker = usePerDomainCircuitBreaker;
18
20
  const FetchClientProvider_js_1 = require("./FetchClientProvider.js");
19
21
  let getCurrentProviderFunc = () => null;
20
22
  /**
@@ -147,3 +149,19 @@ function useRateLimit(options) {
147
149
  function usePerDomainRateLimit(options) {
148
150
  getCurrentProvider().usePerDomainRateLimit(options);
149
151
  }
152
+ /**
153
+ * Enables circuit breaker for any FetchClient instances created by the current provider.
154
+ * The circuit breaker monitors failures and blocks requests when a service is failing.
155
+ * @param options - The circuit breaker configuration options.
156
+ */
157
+ function useCircuitBreaker(options) {
158
+ getCurrentProvider().useCircuitBreaker(options);
159
+ }
160
+ /**
161
+ * Enables per-domain circuit breaker for any FetchClient instances created by the current provider.
162
+ * Each domain gets its own circuit breaker, so failures on one domain don't affect others.
163
+ * @param options - The circuit breaker configuration options.
164
+ */
165
+ function usePerDomainCircuitBreaker(options) {
166
+ getCurrentProvider().usePerDomainCircuitBreaker(options);
167
+ }
@@ -298,7 +298,7 @@ class FetchClient {
298
298
  links: (0, LinkHeader_js_1.parseLinkHeader)(response.headers.get("Link")) || {},
299
299
  };
300
300
  if (getOptions?.cacheKey) {
301
- this.cache.set(getOptions.cacheKey, ctx.response, getOptions.cacheDuration);
301
+ this.cache.set(getOptions.cacheKey, ctx.response, getOptions.cacheDuration, getOptions.cacheTags);
302
302
  }
303
303
  }
304
304
  catch (error) {
@@ -6,19 +6,36 @@ exports.FetchClientCache = void 0;
6
6
  */
7
7
  class FetchClientCache {
8
8
  cache = new Map();
9
+ tagIndex = new Map();
9
10
  /**
10
11
  * Sets a response in the cache with the specified key.
11
12
  * @param key - The cache key.
12
13
  * @param response - The response to be cached.
13
14
  * @param cacheDuration - The duration for which the response should be cached (in milliseconds).
15
+ * @param tags - Optional tags for grouping and invalidating cache entries.
14
16
  */
15
- set(key, response, cacheDuration) {
16
- this.cache.set(this.getHash(key), {
17
+ set(key, response, cacheDuration, tags) {
18
+ const hash = this.getHash(key);
19
+ const normalizedTags = tags ?? [];
20
+ // Remove old tag associations if entry exists
21
+ const existingEntry = this.cache.get(hash);
22
+ if (existingEntry) {
23
+ this.removeTagAssociations(hash, existingEntry.tags);
24
+ }
25
+ this.cache.set(hash, {
17
26
  key,
27
+ tags: normalizedTags,
18
28
  lastAccess: new Date(),
19
29
  expires: new Date(Date.now() + (cacheDuration ?? 60000)),
20
30
  response,
21
31
  });
32
+ // Add new tag associations
33
+ for (const tag of normalizedTags) {
34
+ if (!this.tagIndex.has(tag)) {
35
+ this.tagIndex.set(tag, new Set());
36
+ }
37
+ this.tagIndex.get(tag).add(hash);
38
+ }
22
39
  }
23
40
  /**
24
41
  * Retrieves a response from the cache with the specified key.
@@ -26,12 +43,14 @@ class FetchClientCache {
26
43
  * @returns The cached response, or null if the response is not found or has expired.
27
44
  */
28
45
  get(key) {
29
- const cacheEntry = this.cache.get(this.getHash(key));
46
+ const hash = this.getHash(key);
47
+ const cacheEntry = this.cache.get(hash);
30
48
  if (!cacheEntry) {
31
49
  return null;
32
50
  }
33
51
  if (cacheEntry.expires < new Date()) {
34
- this.cache.delete(this.getHash(key));
52
+ this.removeTagAssociations(hash, cacheEntry.tags);
53
+ this.cache.delete(hash);
35
54
  return null;
36
55
  }
37
56
  cacheEntry.lastAccess = new Date();
@@ -43,7 +62,12 @@ class FetchClientCache {
43
62
  * @returns True if the response was successfully deleted, false otherwise.
44
63
  */
45
64
  delete(key) {
46
- return this.cache.delete(this.getHash(key));
65
+ const hash = this.getHash(key);
66
+ const entry = this.cache.get(hash);
67
+ if (entry) {
68
+ this.removeTagAssociations(hash, entry.tags);
69
+ }
70
+ return this.cache.delete(hash);
47
71
  }
48
72
  /**
49
73
  * Deletes all responses from the cache that have keys beginning with the specified key.
@@ -52,15 +76,56 @@ class FetchClientCache {
52
76
  */
53
77
  deleteAll(prefix) {
54
78
  let count = 0;
55
- for (const key of this.cache.keys()) {
56
- if (key.startsWith(this.getHash(prefix))) {
57
- if (this.cache.delete(key)) {
79
+ const prefixHash = this.getHash(prefix);
80
+ for (const [hash, entry] of this.cache.entries()) {
81
+ if (hash.startsWith(prefixHash)) {
82
+ this.removeTagAssociations(hash, entry.tags);
83
+ if (this.cache.delete(hash)) {
84
+ count++;
85
+ }
86
+ }
87
+ }
88
+ return count;
89
+ }
90
+ /**
91
+ * Deletes all responses from the cache that have the specified tag.
92
+ * @param tag - The cache tag.
93
+ * @returns The number of responses that were deleted.
94
+ */
95
+ deleteByTag(tag) {
96
+ const hashes = this.tagIndex.get(tag);
97
+ if (!hashes) {
98
+ return 0;
99
+ }
100
+ let count = 0;
101
+ for (const hash of hashes) {
102
+ const entry = this.cache.get(hash);
103
+ if (entry) {
104
+ // Remove this entry's associations from all its tags
105
+ this.removeTagAssociations(hash, entry.tags);
106
+ if (this.cache.delete(hash)) {
58
107
  count++;
59
108
  }
60
109
  }
61
110
  }
62
111
  return count;
63
112
  }
113
+ /**
114
+ * Gets all tags currently in use in the cache.
115
+ * @returns An array of all cache tags.
116
+ */
117
+ getTags() {
118
+ return Array.from(this.tagIndex.keys()).filter((tag) => this.tagIndex.get(tag).size > 0);
119
+ }
120
+ /**
121
+ * Gets the tags associated with a cache entry.
122
+ * @param key - The cache key.
123
+ * @returns The tags associated with the entry, or an empty array if not found.
124
+ */
125
+ getEntryTags(key) {
126
+ const entry = this.cache.get(this.getHash(key));
127
+ return entry?.tags ?? [];
128
+ }
64
129
  /**
65
130
  * Checks if a response exists in the cache with the specified key.
66
131
  * @param key - The cache key.
@@ -81,6 +146,7 @@ class FetchClientCache {
81
146
  */
82
147
  clear() {
83
148
  this.cache.clear();
149
+ this.tagIndex.clear();
84
150
  }
85
151
  getHash(key) {
86
152
  if (key instanceof Array) {
@@ -88,5 +154,16 @@ class FetchClientCache {
88
154
  }
89
155
  return key;
90
156
  }
157
+ removeTagAssociations(hash, tags) {
158
+ for (const tag of tags) {
159
+ const hashes = this.tagIndex.get(tag);
160
+ if (hashes) {
161
+ hashes.delete(hash);
162
+ if (hashes.size === 0) {
163
+ this.tagIndex.delete(tag);
164
+ }
165
+ }
166
+ }
167
+ }
91
168
  }
92
169
  exports.FetchClientCache = FetchClientCache;
@@ -7,6 +7,8 @@ const FetchClientCache_js_1 = require("./FetchClientCache.js");
7
7
  const ObjectEvent_js_1 = require("./ObjectEvent.js");
8
8
  const RateLimitMiddleware_js_1 = require("./RateLimitMiddleware.js");
9
9
  const RateLimiter_js_1 = require("./RateLimiter.js");
10
+ const CircuitBreakerMiddleware_js_1 = require("./CircuitBreakerMiddleware.js");
11
+ const CircuitBreaker_js_1 = require("./CircuitBreaker.js");
10
12
  /**
11
13
  * Represents a provider for creating instances of the FetchClient class with shared default options and cache.
12
14
  */
@@ -15,6 +17,9 @@ class FetchClientProvider {
15
17
  #fetch;
16
18
  #cache;
17
19
  #rateLimitMiddleware;
20
+ #rateLimitMiddlewareFunc;
21
+ #circuitBreakerMiddleware;
22
+ #circuitBreakerMiddlewareFunc;
18
23
  #counter = new Counter_js_1.Counter();
19
24
  #onLoading = new ObjectEvent_js_1.ObjectEvent();
20
25
  /**
@@ -171,7 +176,8 @@ class FetchClientProvider {
171
176
  */
172
177
  useRateLimit(options) {
173
178
  this.#rateLimitMiddleware = new RateLimitMiddleware_js_1.RateLimitMiddleware(options);
174
- this.useMiddleware(this.#rateLimitMiddleware.middleware());
179
+ this.#rateLimitMiddlewareFunc = this.#rateLimitMiddleware.middleware();
180
+ this.useMiddleware(this.#rateLimitMiddlewareFunc);
175
181
  }
176
182
  /**
177
183
  * Enables rate limiting for all FetchClient instances created by this provider.
@@ -182,7 +188,8 @@ class FetchClientProvider {
182
188
  ...options,
183
189
  getGroupFunc: RateLimiter_js_1.groupByDomain,
184
190
  });
185
- this.useMiddleware(this.#rateLimitMiddleware.middleware());
191
+ this.#rateLimitMiddlewareFunc = this.#rateLimitMiddleware.middleware();
192
+ this.useMiddleware(this.#rateLimitMiddlewareFunc);
186
193
  }
187
194
  /**
188
195
  * Gets the rate limiter instance used for rate limiting.
@@ -195,8 +202,56 @@ class FetchClientProvider {
195
202
  * Removes the rate limiting middleware from all FetchClient instances created by this provider.
196
203
  */
197
204
  removeRateLimit() {
205
+ const middlewareFunc = this.#rateLimitMiddlewareFunc;
198
206
  this.#rateLimitMiddleware = undefined;
199
- this.#options.middleware = this.#options.middleware?.filter((m) => !(m instanceof RateLimitMiddleware_js_1.RateLimitMiddleware));
207
+ this.#rateLimitMiddlewareFunc = undefined;
208
+ if (middlewareFunc) {
209
+ this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
210
+ }
211
+ }
212
+ /**
213
+ * Enables circuit breaker for all FetchClient instances created by this provider.
214
+ * The circuit breaker monitors failures and blocks requests when a service is failing,
215
+ * allowing time for recovery.
216
+ * @param options - The circuit breaker configuration options.
217
+ */
218
+ useCircuitBreaker(options) {
219
+ this.#circuitBreakerMiddleware = new CircuitBreakerMiddleware_js_1.CircuitBreakerMiddleware(options);
220
+ this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware
221
+ .middleware();
222
+ this.useMiddleware(this.#circuitBreakerMiddlewareFunc);
223
+ }
224
+ /**
225
+ * Enables per-domain circuit breaker for all FetchClient instances created by this provider.
226
+ * Each domain gets its own circuit breaker, so failures on one domain don't affect others.
227
+ * @param options - The circuit breaker configuration options.
228
+ */
229
+ usePerDomainCircuitBreaker(options) {
230
+ this.#circuitBreakerMiddleware = new CircuitBreakerMiddleware_js_1.CircuitBreakerMiddleware({
231
+ ...options,
232
+ getGroupFunc: CircuitBreaker_js_1.groupByDomain,
233
+ });
234
+ this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware
235
+ .middleware();
236
+ this.useMiddleware(this.#circuitBreakerMiddlewareFunc);
237
+ }
238
+ /**
239
+ * Gets the circuit breaker instance.
240
+ * @returns The circuit breaker instance, or undefined if not enabled.
241
+ */
242
+ get circuitBreaker() {
243
+ return this.#circuitBreakerMiddleware?.circuitBreaker;
244
+ }
245
+ /**
246
+ * Removes the circuit breaker middleware from all FetchClient instances created by this provider.
247
+ */
248
+ removeCircuitBreaker() {
249
+ const middlewareFunc = this.#circuitBreakerMiddlewareFunc;
250
+ this.#circuitBreakerMiddleware = undefined;
251
+ this.#circuitBreakerMiddlewareFunc = undefined;
252
+ if (middlewareFunc) {
253
+ this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
254
+ }
200
255
  }
201
256
  }
202
257
  exports.FetchClientProvider = FetchClientProvider;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MockHistoryImpl = void 0;
4
+ /**
5
+ * Implementation of MockHistory that tracks recorded requests.
6
+ */
7
+ class MockHistoryImpl {
8
+ #get = [];
9
+ #post = [];
10
+ #put = [];
11
+ #patch = [];
12
+ #delete = [];
13
+ #all = [];
14
+ get get() {
15
+ return [...this.#get];
16
+ }
17
+ get post() {
18
+ return [...this.#post];
19
+ }
20
+ get put() {
21
+ return [...this.#put];
22
+ }
23
+ get patch() {
24
+ return [...this.#patch];
25
+ }
26
+ get delete() {
27
+ return [...this.#delete];
28
+ }
29
+ get all() {
30
+ return [...this.#all];
31
+ }
32
+ /**
33
+ * Records a request in the history.
34
+ */
35
+ record(request) {
36
+ this.#all.push(request);
37
+ switch (request.method.toUpperCase()) {
38
+ case "GET":
39
+ this.#get.push(request);
40
+ break;
41
+ case "POST":
42
+ this.#post.push(request);
43
+ break;
44
+ case "PUT":
45
+ this.#put.push(request);
46
+ break;
47
+ case "PATCH":
48
+ this.#patch.push(request);
49
+ break;
50
+ case "DELETE":
51
+ this.#delete.push(request);
52
+ break;
53
+ }
54
+ }
55
+ /**
56
+ * Clears all recorded history.
57
+ */
58
+ clear() {
59
+ this.#get = [];
60
+ this.#post = [];
61
+ this.#put = [];
62
+ this.#patch = [];
63
+ this.#delete = [];
64
+ this.#all = [];
65
+ }
66
+ }
67
+ exports.MockHistoryImpl = MockHistoryImpl;