Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/named-types-and-pending-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@http-client-toolkit/core': minor
---

Export `HttpClientObservabilityOptions` and `PerRequestCacheOptions` as named types so consuming SDKs can reference them directly instead of re-deriving inline shapes. `HttpClientOptions.observability` and the per-request `get()` `cache` option now use these named types.

Add a synchronous `getPendingRequestCount(resourceKey?)` accessor on
`HttpClient` (and `HttpClientContract`) returning the number of in-flight
`get()` calls. Includes both executing requests and joiners waiting on a
deduplicated request. Pass a `resourceKey` (as produced by
`resourceKeyResolver`, or the URL origin by default) to scope the count to a
single rate-limit bucket; omit it for the total across all resources.
Complements the asynchronous `dedupe:owner` / `dedupe:join` observability
events as a synchronous queue-depth probe.
58 changes: 55 additions & 3 deletions docs-site/src/content/docs/api/http-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ new HttpClient(options);
| `responseHandler` | `(data: unknown) => unknown` | — | Post-transformation hook for validation or domain-level error detection. Throw to reject 2xx responses with application-level errors |
| `errorHandler` | `(context: HttpErrorContext) => Error` | — | Convert HTTP errors to domain-specific types. Context includes `url`, response `status`, parsed `data`, and `headers`. Not called for network failures |
| `retry` | `RetryOptions \| false` | — | Automatic retry configuration. See [Retries guide](/http-client-toolkit/guides/retries/) |
| `observability` | `{ onEvent?: (event: HttpClientEvent) => void \| Promise<void> }` | — | Subscribe to read-only structured lifecycle events |
| `observability` | `HttpClientObservabilityOptions` | — | Subscribe to read-only structured lifecycle events |

#### `cache` Options (`HttpClientCacheOptions`)

Expand Down Expand Up @@ -82,7 +82,7 @@ The `url` must be an absolute URL (e.g. `https://api.example.com/items`).
| `priority` | `'user' \| 'background'` | `'background'` | Used by adaptive rate-limit stores |
| `headers` | `Record<string, string>` | — | Custom headers sent with the request; also used for Vary-based cache matching |
| `retry` | `RetryOptions \| false` | — | Per-request retry override. Pass `false` to disable retries for this request |
| `cache` | `{ ttl?: number; overrides?: CacheOverrideOptions; tags?: string[] }` | — | Per-request cache options. `ttl` overrides constructor TTL; `overrides` are shallow-merged with constructor-level; `tags` associates the cached entry with tags for later invalidation |
| `cache` | `PerRequestCacheOptions` | — | Per-request cache options. `ttl` overrides constructor TTL; `overrides` are shallow-merged with constructor-level; `tags` associates the cached entry with tags for later invalidation |

## Request Flow

Expand All @@ -102,7 +102,7 @@ See the [Interceptors guide](/http-client-toolkit/guides/interceptors/) for deta

## Observability

Use `observability.onEvent` to collect request lifecycle events for logs, metrics, or tracing without wrapping internal stores or fetch calls.
Use `observability.onEvent` (typed as `HttpClientObservabilityOptions`) to collect request lifecycle events for logs, metrics, or tracing without wrapping internal stores or fetch calls.

```typescript
import { HttpClient, type HttpClientEvent } from '@http-client-toolkit/core';
Expand Down Expand Up @@ -240,6 +240,58 @@ Waits for all pending `stale-while-revalidate` background fetches to complete. U
await client.flushRevalidations();
```

### `getPendingRequestCount(resourceKey?)`

Returns the number of `get()` calls currently in-flight on this client. Includes both requests being executed by this client and requests joined onto an in-flight deduplicated request.

```typescript
// Total across all resources
const total = client.getPendingRequestCount();
```

Pass a `resourceKey` — the same value `resourceKeyResolver` returns — to scope the count to a single rate-limit bucket. `HttpClient` is not bound to a base URL, so with the default resolver the resource key is derived from each request URL's origin and all paths on the same origin share one bucket:

```typescript
// Counts only in-flight requests to this origin. This differs from
// getPendingRequestCount() only when the same client instance also has
// requests in-flight for other origins or custom resource buckets.
const github = client.getPendingRequestCount('https://api.github.com');
```

For REST APIs, individual resources/endpoints only get separate counts when you configure a custom `resourceKeyResolver` that returns finer-grained keys than the URL origin — the same pattern used for rate-limit bucketing:

```typescript
import { HttpClient } from '@http-client-toolkit/core';

const client = new HttpClient({
name: 'issues-api',
resourceKeyResolver: (url) => {
const path = new URL(url).pathname;
if (path === '/api/issues' || path.startsWith('/api/issue/')) {
return 'issues';
}
if (path.startsWith('/api/users')) {
return 'users';
}
return new URL(url).origin;
},
});

// Per-bucket backpressure: throttle the issues endpoint independently
// of user lookups, even though they share a client.
if (client.getPendingRequestCount('issues') >= 50) {
throw new Error('Issues endpoint queue saturated');
}

// User lookups are tracked separately because the resolver returns 'users'.
const pendingUsers = client.getPendingRequestCount('users');

// Total load across the whole client
const total = client.getPendingRequestCount();
```

This is a synchronous queue-depth probe complementing the asynchronous `dedupe:owner` / `dedupe:join` observability events — useful for backpressure, concurrency limiting, or scheduling decisions where waiting for an event would be too late.

## Examples

### Cache-Only Client
Expand Down
26 changes: 14 additions & 12 deletions docs-site/src/content/docs/api/utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ try {

The `@http-client-toolkit/core` package exports:

| Export | Type | Description |
| ---------------------------- | --------- | ------------------------------------------------------------------- |
| `HttpClient` | Class | Main client class |
| `HttpClientError` | Class | Error class with `statusCode` |
| `HttpClientEvent` | Type | Stable structured lifecycle event union for `observability.onEvent` |
| `hashRequest` | Function | Deterministic SHA-256 request hashing |
| `HttpClientCacheOptions` | Interface | Cache configuration for `HttpClientOptions.cache` |
| `HttpClientRateLimitOptions` | Interface | Rate limit configuration for `HttpClientOptions.rateLimit` |
| `CacheStore` | Interface | Cache store contract |
| `DedupeStore` | Interface | Deduplication store contract |
| `RateLimitStore` | Interface | Rate limit store contract |
| `AdaptiveRateLimitStore` | Interface | Adaptive rate limit store contract |
| Export | Type | Description |
| ----------------------------- | --------- | ---------------------------------------------------------------------------------- |
| `HttpClient` | Class | Main client class |
| `HttpClientError` | Class | Error class with `statusCode` |
| `HttpClientEvent` | Type | Stable structured lifecycle event union for `observability.onEvent` |
| `HttpClientObservabilityOptions` | Type | Named type for `HttpClientOptions.observability` |
| `PerRequestCacheOptions` | Type | Named type for the per-request `cache` option on `get()` |
| `hashRequest` | Function | Deterministic SHA-256 request hashing |
| `HttpClientCacheOptions` | Interface | Cache configuration for `HttpClientOptions.cache` |
| `HttpClientRateLimitOptions` | Interface | Rate limit configuration for `HttpClientOptions.rateLimit` |
| `CacheStore` | Interface | Cache store contract |
| `DedupeStore` | Interface | Deduplication store contract |
| `RateLimitStore` | Interface | Rate limit store contract |
| `AdaptiveRateLimitStore` | Interface | Adaptive rate limit store contract |
64 changes: 45 additions & 19 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,23 @@ Create a thin wrapper module per third-party API so callers don't configure anyt

**Constructor options:**

| Property | Type | Default | Description |
| --------------------- | ----------------------------------------------------------------- | ---------- | -------------------------------------------------- |
| `name` | `string` | required | Name for the client instance |
| `cache` | `CacheStore` | - | Response caching |
| `dedupe` | `DedupeStore` | - | Request deduplication |
| `rateLimit` | `RateLimitStore \| AdaptiveRateLimitStore` | - | Rate limiting |
| `cacheTTL` | `number` | `3600` | Cache TTL when response has no headers |
| `throwOnRateLimit` | `boolean` | `true` | Throw when rate limited vs. wait |
| `maxWaitTime` | `number` | `60000` | Max wait time (ms) before throwing |
| `responseTransformer` | `(data: unknown) => unknown` | - | Transform raw response data |
| `responseHandler` | `(data: unknown) => unknown` | - | Validate/process transformed data |
| `errorHandler` | `(error: unknown) => Error` | - | Convert errors to domain-specific types |
| `cacheOverrides` | `CacheOverrideOptions` | - | Override cache header behaviors |
| `retry` | `RetryOptions \| false` | - | Retry config; `false` disables globally |
| `rateLimitHeaders` | `RateLimitHeaderConfig` | defaults | Configure standard/custom header names |
| `resourceKeyResolver` | `(url: string) => string` | URL origin | Customize how rate-limit resource keys are derived |
| `observability` | `{ onEvent?: (event: HttpClientEvent) => void \| Promise<void> }` | - | Subscribe to structured lifecycle events |
| Property | Type | Default | Description |
| --------------------- | ------------------------------------------ | ---------- | -------------------------------------------------- |
| `name` | `string` | required | Name for the client instance |
| `cache` | `CacheStore` | - | Response caching |
| `dedupe` | `DedupeStore` | - | Request deduplication |
| `rateLimit` | `RateLimitStore \| AdaptiveRateLimitStore` | - | Rate limiting |
| `cacheTTL` | `number` | `3600` | Cache TTL when response has no headers |
| `throwOnRateLimit` | `boolean` | `true` | Throw when rate limited vs. wait |
| `maxWaitTime` | `number` | `60000` | Max wait time (ms) before throwing |
| `responseTransformer` | `(data: unknown) => unknown` | - | Transform raw response data |
| `responseHandler` | `(data: unknown) => unknown` | - | Validate/process transformed data |
| `errorHandler` | `(error: unknown) => Error` | - | Convert errors to domain-specific types |
| `cacheOverrides` | `CacheOverrideOptions` | - | Override cache header behaviors |
| `retry` | `RetryOptions \| false` | - | Retry config; `false` disables globally |
| `rateLimitHeaders` | `RateLimitHeaderConfig` | defaults | Configure standard/custom header names |
| `resourceKeyResolver` | `(url: string) => string` | URL origin | Customize how rate-limit resource keys are derived |
| `observability` | `HttpClientObservabilityOptions` | - | Subscribe to structured lifecycle events |

### Request Flow

Expand Down Expand Up @@ -202,8 +202,8 @@ const client = new HttpClient({

### Custom Rate-Limit Buckets

Use `resourceKeyResolver` when list and retrieve routes should share the same
rate-limit bucket:
Use `resourceKeyResolver` when REST routes should map to endpoint-level
rate-limit buckets instead of the default origin-level bucket:

```typescript
const client = new HttpClient({
Expand All @@ -213,6 +213,9 @@ const client = new HttpClient({
if (path === '/api/issues' || path.startsWith('/api/issue/')) {
return 'issues';
}
if (path.startsWith('/api/users')) {
return 'users';
}
return new URL(url).origin;
},
rateLimit: {
Expand All @@ -229,9 +232,32 @@ compatibility.
- `HttpClient` - Main client class
- `HttpClientError` - Error class with `statusCode`
- `HttpClientEvent` - Stable structured lifecycle event union
- `HttpClientObservabilityOptions` - Named type for `HttpClientOptions.observability`
- `PerRequestCacheOptions` - Named type for `get()` `cache` option shape
- `hashRequest` - Deterministic SHA-256 request hashing
- Store interfaces: `CacheStore`, `DedupeStore`, `RateLimitStore`, `AdaptiveRateLimitStore`

`HttpClient` exposes a synchronous `getPendingRequestCount(resourceKey?)`
accessor that returns the number of `get()` calls currently in-flight on the
client. This includes both requests being executed and requests joined onto an
in-flight deduplicated request.

Pass a `resourceKey` as produced by `resourceKeyResolver` to scope the count to
a single rate-limit bucket. `HttpClient` is not bound to a base URL, so with the
default resolver that key is derived from each request URL's origin and all
paths on the same origin share one bucket; omit the key for the total across
all resources on the client. For per-endpoint REST backpressure, configure
`resourceKeyResolver` to return finer-grained keys for those routes. Use this
accessor as a synchronous queue-depth probe complementing the asynchronous
`dedupe:owner` / `dedupe:join` observability events.

```typescript
// With the resolver above, these are separate endpoint-level buckets.
const pendingIssues = client.getPendingRequestCount('issues');
const pendingUsers = client.getPendingRequestCount('users');
const pendingTotal = client.getPendingRequestCount();
```

## License

ISC
Loading