Skip to content

Commit 660c0a2

Browse files
committed
More docs updates
1 parent 1a66229 commit 660c0a2

File tree

8 files changed

+85
-55
lines changed

8 files changed

+85
-55
lines changed

.claude/settings.local.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
/npm
33
*.lcov
44
/cov
5+
6+
nul

docs/index.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,28 @@ hero:
1515
text: View on GitHub
1616
link: https://github.com/FoundatioFx/FetchClient
1717
features:
18-
- icon: "\u26A1"
18+
- icon:
1919
title: Typed JSON Helpers
2020
details: getJSON, postJSON, putJSON, patchJSON, deleteJSON with full TypeScript support.
21-
- icon: "\uD83C\uDFAF"
21+
- icon: 🎯
2222
title: Two API Styles
2323
details: Use simple functions or classes - your choice. Both have full access to all features.
24-
- icon: "\uD83D\uDCBE"
24+
- icon: 💾
2525
title: Response Caching
2626
details: TTL-based caching with cache keys and tags for grouped invalidation.
27-
- icon: "\uD83E\uDDE9"
27+
- icon: 🧩
2828
title: Middleware
2929
details: Intercept requests and responses for logging, auth, transforms, and more.
30-
- icon: "\uD83D\uDEA6"
30+
- icon: 🚦
3131
title: Rate Limiting
3232
details: Per-domain rate limits with automatic API header detection.
33-
- icon: "\uD83D\uDEE1\uFE0F"
33+
- icon: 🛡️
3434
title: Circuit Breaker
3535
details: Prevent cascading failures when services go down.
36-
- icon: "\u23F1\uFE0F"
36+
- icon: ⏱️
3737
title: Timeouts & Cancellation
3838
details: Request timeouts with native AbortSignal support.
39-
- icon: "\uD83E\uDDEA"
39+
- icon: 🧪
4040
title: Testing Built-in
4141
details: MockRegistry for mocking HTTP requests without network calls.
4242
---

mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export {
1919
CircuitBreaker,
2020
type CircuitBreakerOptions,
2121
type CircuitState,
22-
type GroupCircuitBreakerOptions,
2322
groupByDomain as circuitBreakerGroupByDomain,
23+
type GroupCircuitBreakerOptions,
2424
} from "./src/CircuitBreaker.ts";
2525
export {
2626
CircuitBreakerMiddleware,

readme.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
[![Discord](https://img.shields.io/discord/715744504891703319)](https://discord.gg/6HxgFCx)
88

99
FetchClient is a tiny, typed wrapper around `fetch` with JSON helpers, caching,
10-
middleware, rate limiting, circuit breaker, timeouts, and friendly error handling.
10+
middleware, rate limiting, circuit breaker, timeouts, and friendly error
11+
handling.
1112

1213
## Features
1314

14-
- **Typed JSON helpers** - `getJSON`, `postJSON`, `putJSON`, `patchJSON`, `deleteJSON`
15+
- **Typed JSON helpers** - `getJSON`, `postJSON`, `putJSON`, `patchJSON`,
16+
`deleteJSON`
1517
- **Two API styles** - Functional (no classes) or class-based - your choice
1618
- **Response caching** - TTL-based caching with tags for grouped invalidation
1719
- **Middleware** - Intercept requests/responses for logging, auth, transforms
@@ -53,7 +55,9 @@ setBaseUrl("https://api.example.com");
5355

5456
const client = getFetchClient();
5557
const { data: users } = await client.getJSON<User[]>("/users");
56-
const { data: created } = await client.postJSON<User>("/users", { name: "Alice" });
58+
const { data: created } = await client.postJSON<User>("/users", {
59+
name: "Alice",
60+
});
5761
```
5862

5963
### Class-Based API
@@ -65,7 +69,8 @@ const client = new FetchClient({ baseUrl: "https://api.example.com" });
6569
const { data } = await client.getJSON<User[]>("/users");
6670
```
6771

68-
All styles share the same configuration - the functional API wraps a [default provider](https://fetchclient.foundatio.dev/guide/provider#default-provider).
72+
All styles share the same configuration - the functional API wraps a
73+
[default provider](https://fetchclient.foundatio.dev/guide/provider#default-provider).
6974

7075
## Caching
7176

@@ -111,8 +116,8 @@ import { FetchClientProvider } from "@foundatiofx/fetchclient";
111116

112117
const provider = new FetchClientProvider();
113118
provider.useCircuitBreaker({
114-
failureThreshold: 5, // Open after 5 failures
115-
openDurationMs: 30000, // Stay open for 30 seconds
119+
failureThreshold: 5, // Open after 5 failures
120+
openDurationMs: 30000, // Stay open for 30 seconds
116121
});
117122

118123
// When API fails repeatedly, circuit opens

src/CircuitBreaker.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,16 @@ interface CircuitBreakerBucket {
4848
halfOpenAttempts: number; // Current concurrent requests in HALF_OPEN
4949
}
5050

51-
type RequiredOptions = Required<
52-
Omit<CircuitBreakerOptions, "groups" | "onOpen" | "onClose" | "onHalfOpen" | "onStateChange">
53-
> & {
54-
onStateChange?: (from: CircuitState, to: CircuitState) => void;
55-
};
51+
type RequiredOptions =
52+
& Required<
53+
Omit<
54+
CircuitBreakerOptions,
55+
"groups" | "onOpen" | "onClose" | "onHalfOpen" | "onStateChange"
56+
>
57+
>
58+
& {
59+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
60+
};
5661

5762
/**
5863
* Circuit breaker for preventing cascading failures.
@@ -117,16 +122,22 @@ export class CircuitBreaker {
117122
/**
118123
* Gets the effective options for a group.
119124
*/
120-
#getOptions(group: string): Required<Omit<GroupCircuitBreakerOptions, "onStateChange">> & {
125+
#getOptions(
126+
group: string,
127+
): Required<Omit<GroupCircuitBreakerOptions, "onStateChange">> & {
121128
onStateChange?: (from: CircuitState, to: CircuitState) => void;
122129
} {
123130
const groupOpts = this.#groupOptions.get(group);
124131
return {
125-
failureThreshold: groupOpts?.failureThreshold ?? this.#options.failureThreshold,
126-
failureWindowMs: groupOpts?.failureWindowMs ?? this.#options.failureWindowMs,
132+
failureThreshold: groupOpts?.failureThreshold ??
133+
this.#options.failureThreshold,
134+
failureWindowMs: groupOpts?.failureWindowMs ??
135+
this.#options.failureWindowMs,
127136
openDurationMs: groupOpts?.openDurationMs ?? this.#options.openDurationMs,
128-
successThreshold: groupOpts?.successThreshold ?? this.#options.successThreshold,
129-
halfOpenMaxAttempts: groupOpts?.halfOpenMaxAttempts ?? this.#options.halfOpenMaxAttempts,
137+
successThreshold: groupOpts?.successThreshold ??
138+
this.#options.successThreshold,
139+
halfOpenMaxAttempts: groupOpts?.halfOpenMaxAttempts ??
140+
this.#options.halfOpenMaxAttempts,
130141
onStateChange: groupOpts?.onStateChange ?? this.#options.onStateChange,
131142
};
132143
}
@@ -152,7 +163,11 @@ export class CircuitBreaker {
152163
/**
153164
* Transitions the circuit to a new state.
154165
*/
155-
#transitionTo(group: string, bucket: CircuitBreakerBucket, newState: CircuitState): void {
166+
#transitionTo(
167+
group: string,
168+
bucket: CircuitBreakerBucket,
169+
newState: CircuitState,
170+
): void {
156171
const oldState = bucket.state;
157172
if (oldState === newState) return;
158173

src/FetchClientProvider.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ export class FetchClientProvider {
260260
*/
261261
public useCircuitBreaker(options?: CircuitBreakerMiddlewareOptions) {
262262
this.#circuitBreakerMiddleware = new CircuitBreakerMiddleware(options);
263-
this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware.middleware();
263+
this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware
264+
.middleware();
264265
this.useMiddleware(this.#circuitBreakerMiddlewareFunc);
265266
}
266267

@@ -276,7 +277,8 @@ export class FetchClientProvider {
276277
...options,
277278
getGroupFunc: circuitBreakerGroupByDomain,
278279
});
279-
this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware.middleware();
280+
this.#circuitBreakerMiddlewareFunc = this.#circuitBreakerMiddleware
281+
.middleware();
280282
this.useMiddleware(this.#circuitBreakerMiddlewareFunc);
281283
}
282284

src/tests/Provider.test.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,19 @@ Deno.test("FetchClientProvider - useRateLimit enables rate limiting", async () =
283283
const client = provider.getFetchClient();
284284

285285
// First two requests should succeed
286-
const response1 = await client.getJSON("/api/data", { expectedStatusCodes: [429] });
287-
const response2 = await client.getJSON("/api/data", { expectedStatusCodes: [429] });
286+
const response1 = await client.getJSON("/api/data", {
287+
expectedStatusCodes: [429],
288+
});
289+
const response2 = await client.getJSON("/api/data", {
290+
expectedStatusCodes: [429],
291+
});
288292
assertEquals(response1.status, 200);
289293
assertEquals(response2.status, 200);
290294

291295
// Third request should be rate limited
292-
const response3 = await client.getJSON("/api/data", { expectedStatusCodes: [429] });
296+
const response3 = await client.getJSON("/api/data", {
297+
expectedStatusCodes: [429],
298+
});
293299
assertEquals(response3.status, 429);
294300

295301
mocks.restore();
@@ -313,7 +319,9 @@ Deno.test("FetchClientProvider - removeRateLimit disables rate limiting", async
313319
await client.getJSON("/api/data", { expectedStatusCodes: [429] });
314320

315321
// Second would be rate limited
316-
const response2 = await client.getJSON("/api/data", { expectedStatusCodes: [429] });
322+
const response2 = await client.getJSON("/api/data", {
323+
expectedStatusCodes: [429],
324+
});
317325
assertEquals(response2.status, 429);
318326

319327
// Remove rate limiting
@@ -351,7 +359,9 @@ Deno.test("FetchClientProvider - useCircuitBreaker enables circuit breaker", asy
351359
assertEquals(provider.circuitBreaker!.getState("/api/data"), "OPEN");
352360

353361
// Next request should return 503 without hitting the API
354-
const response = await client.getJSON("/api/data", { expectedStatusCodes: [503] });
362+
const response = await client.getJSON("/api/data", {
363+
expectedStatusCodes: [503],
364+
});
355365
assertEquals(response.status, 503);
356366
assertEquals(mocks.history.get.length, 2); // Only 2 requests made
357367

@@ -381,7 +391,9 @@ Deno.test("FetchClientProvider - removeCircuitBreaker disables circuit breaker",
381391

382392
// Now requests should go through (need new client)
383393
const client2 = provider.getFetchClient();
384-
const response = await client2.getJSON("/api/data", { expectedStatusCodes: [500] });
394+
const response = await client2.getJSON("/api/data", {
395+
expectedStatusCodes: [500],
396+
});
385397
assertEquals(response.status, 500); // Actual response, not 503
386398

387399
mocks.restore();
@@ -466,15 +478,21 @@ Deno.test("FetchClientProvider - usePerDomainRateLimit groups by domain", async
466478
const client = provider.getFetchClient();
467479

468480
// First request to domain1 succeeds
469-
const r1 = await client.getJSON("https://domain1.com/api/data", { expectedStatusCodes: [429] });
481+
const r1 = await client.getJSON("https://domain1.com/api/data", {
482+
expectedStatusCodes: [429],
483+
});
470484
assertEquals(r1.status, 200);
471485

472486
// Second request to domain1 is rate limited
473-
const r2 = await client.getJSON("https://domain1.com/api/other", { expectedStatusCodes: [429] });
487+
const r2 = await client.getJSON("https://domain1.com/api/other", {
488+
expectedStatusCodes: [429],
489+
});
474490
assertEquals(r2.status, 429);
475491

476492
// First request to domain2 succeeds (different domain)
477-
const r3 = await client.getJSON("https://domain2.com/api/data", { expectedStatusCodes: [429] });
493+
const r3 = await client.getJSON("https://domain2.com/api/data", {
494+
expectedStatusCodes: [429],
495+
});
478496
assertEquals(r3.status, 200);
479497

480498
mocks.restore();
@@ -495,10 +513,14 @@ Deno.test("FetchClientProvider - usePerDomainCircuitBreaker isolates domains", a
495513
const client = provider.getFetchClient();
496514

497515
// Fail on failing.com to open its circuit
498-
await client.getJSON("https://failing.com/api", { expectedStatusCodes: [500, 503] });
516+
await client.getJSON("https://failing.com/api", {
517+
expectedStatusCodes: [500, 503],
518+
});
499519

500520
// failing.com circuit is open
501-
const r1 = await client.getJSON("https://failing.com/api", { expectedStatusCodes: [503] });
521+
const r1 = await client.getJSON("https://failing.com/api", {
522+
expectedStatusCodes: [503],
523+
});
502524
assertEquals(r1.status, 503);
503525

504526
// working.com should still work (separate circuit)

0 commit comments

Comments
 (0)