Skip to content
Open
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
40 changes: 36 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ Before updating docs for a release, inspect the package source and tests instead
- `packages/client/src/index.ts`
- `packages/client/src/types/*`
- `packages/client/src/errors/*`
- `packages/client/src/adapters/*`
- `packages/client/src/core/request.ts`
- `packages/client/src/core/should-retry.ts`
- `packages/client/src/core/resolve-runtime-config.ts`
- `packages/client/src/core/resolve-response-validator.ts`
- `packages/client/src/core/telemetry.ts`
- `packages/client/package.json` (exports, peer dependencies)
- `packages/client/tests`

If behavior is unclear, check the release branch or ask before documenting it.
Expand Down Expand Up @@ -67,11 +72,13 @@ For example:
- Installation
- Create Client
- Response Handling
- Serialization
- Auth
- Hooks
- Observability
- Retry
- Errors
- Extensibility
- Examples
- API Reference

Expand All @@ -80,15 +87,40 @@ For example:
Keep these names and behaviors consistent across the docs:

- `baseUrl`, never `baseURL`
- `createClient`
- `createClient`, `createTelemetryHooks`
- `get`, `delete`, `post`, `put`, `patch`, `request`
- auth strategies: `bearer`, `apiKey`, `custom`
- retry config: `attempts`, `retryOn`, `retryMethods`, `backoff`, `baseDelayMs`
- request metadata: `requestId`, `x-request-id`
- auth interfaces: `AuthProvider`, `AuthContext`
- retry config: `attempts`, `retryOn`, `retryMethods`, `backoff`, `baseDelayMs`, `jitter`,
`maxElapsedMs`, `shouldRetry`
- retry interfaces: `RetryPredicate`, `RetryPredicateContext`
- request metadata: `requestId`, `x-request-id`, `operationName`
- idempotency: `idempotencyKey`, `idempotency-key`
- response validation: `validateResponse`, `ResponseValidator`, `ValidationError`
- response validation: `validateResponse`, `ResponseValidator`, `responseSchema`,
`ValidationAdapter`, `ValidationResult`, `ValidationError`
- validation adapters: `zodAdapter` from `@dfsync/client/adapters/zod`
- serialization: `serializeBody`, `parseResponse`, `BodySerializer`, `ResponseParser`,
`SerializedBody`, `SerializerContext`
- hooks: `beforeRequest`, `afterResponse`, `onError`, `onRetry`
- telemetry: `TelemetryExporter`, `onRequestStart`, `onRequestSuccess`, `onRequestError`,
`onRequestRetry`
- errors: `DfsyncError`, `HttpError`, `NetworkError`, `TimeoutError`, `ValidationError`, `RequestAbortedError`
- error metadata: `requestId`, `attempt`, `durationMs`, and `issues` on `ValidationError`

## Behavioral rules that are easy to get wrong

- validation runs only after a successful HTTP response, and validation failures are never retried
- validator precedence: request `responseSchema` → request `validateResponse` →
client `responseSchema` → client `validateResponse`
- `responseSchema` does not transform the response in this version, even when the adapter
returns `data`
- `retry.shouldRetry` runs only after the built-in rules already allow a retry, so it can
restrict retries but never widen them
- `maxElapsedMs` is checked against elapsed time plus the next planned delay
- `jitter` never applies to `Retry-After` delays
- `parseResponse` runs for every response, before classification and validation
- a serializer's `contentType` is applied only when no `content-type` header is set
- `operationName` is context-only and is never sent as a header

## Release docs checklist

Expand Down
228 changes: 198 additions & 30 deletions docs/client/v1/api-reference.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
# API Reference

## Exports

```ts
import {
createClient,
createTelemetryHooks,
DfsyncError,
HttpError,
NetworkError,
TimeoutError,
ValidationError,
RequestAbortedError,
} from '@dfsync/client';
```

Subpath exports:

```ts
import { zodAdapter } from '@dfsync/client/adapters/zod';
```

## createClient

Creates a configured HTTP client.

```ts
import { createClient } from '@dfsync/client';

const client = createClient({ baseUrl: 'https://api.example.com' });
```

## createTelemetryHooks

Maps a `TelemetryExporter` onto a `HooksConfig`.

```ts
import { createClient, createTelemetryHooks } from '@dfsync/client';

const client = createClient({
baseUrl: 'https://api.example.com',
hooks: createTelemetryHooks(exporter),
});
```

## Client methods
Expand All @@ -23,38 +59,104 @@ client.request(config)

## Configuration

- `baseUrl`
- `timeout`
- `headers`
- `fetch`
- `retry`
- `auth`
- `hooks`
- `validateResponse`
| Option | Type | Description |
| ------------------ | ----------------------- | --------------------------------- |
| `baseUrl` | `string` | required base URL |
| `timeout` | `number` | request timeout, default `5000` |
| `headers` | `Record<string,string>` | default headers |
| `fetch` | `typeof fetch` | custom fetch implementation |
| `auth` | `AuthConfig` | auth strategy |
| `hooks` | `HooksConfig` | lifecycle hooks |
| `retry` | `RetryConfig` | retry policy |
| `validateResponse` | `ResponseValidator` | predicate response validation |
| `responseSchema` | `ValidationAdapter` | schema-based response validation |
| `serializeBody` | `BodySerializer` | custom request body serialization |
| `parseResponse` | `ResponseParser` | custom response parsing |

## Request options

- `query`
- `headers`
- `timeout`
- `retry`
- `signal`
- `requestId`
- `idempotencyKey`
- `validateResponse`
| Option | Type | Description |
| ------------------ | ------------------- | -------------------------------- |
| `query` | `QueryParams` | query string values |
| `headers` | `HeadersMap` | request headers |
| `timeout` | `number` | per-request timeout |
| `retry` | `RetryConfig` | per-request retry policy |
| `signal` | `AbortSignal` | external cancellation |
| `requestId` | `string` | explicit request identifier |
| `operationName` | `string` | human-readable operation label |
| `idempotencyKey` | `string` | idempotency key for safe retries |
| `validateResponse` | `ResponseValidator` | per-request predicate validation |
| `responseSchema` | `ValidationAdapter` | per-request schema validation |
| `serializeBody` | `BodySerializer` | per-request body serialization |
| `parseResponse` | `ResponseParser` | per-request response parsing |

`client.request(config)` additionally takes `method`, `path`, and `body`.

## Retry

- `attempts`
- `backoff`
- `baseDelayMs`
- `retryOn`
- `retryMethods`
| Option | Type | Default |
| -------------- | ------------------ | -------------------------- |
| `attempts` | `number` | `0` |
| `backoff` | `RetryBackoff` | `'exponential'` |
| `baseDelayMs` | `number` | `300` |
| `retryOn` | `RetryCondition[]` | `['network-error', '5xx']` |
| `retryMethods` | `RequestMethod[]` | `['GET', 'PUT', 'DELETE']` |
| `jitter` | `boolean` | `false` |
| `maxElapsedMs` | `number` | not set |
| `shouldRetry` | `RetryPredicate` | not set |

```ts
type RetryCondition = 'network-error' | '5xx' | '429';
type RetryBackoff = 'fixed' | 'exponential';

type RetryPredicateContext = {
error: Error;
attempt: number;
maxAttempts: number;
requestId: string;
method: RequestMethod;
};

type RetryPredicate = (ctx: RetryPredicateContext) => boolean;
```

## Response validation

```ts
type ResponseValidator<TData = unknown> = (data: TData) => boolean | void | Promise<boolean | void>;

type ValidationResult<TData = unknown> = {
success: boolean;
data?: TData;
error?: unknown;
};

type ValidationAdapter<TData = unknown> = (
data: unknown,
) => ValidationResult<TData> | Promise<ValidationResult<TData>>;
```

Precedence: request `responseSchema` → request `validateResponse` → client `responseSchema` → client `validateResponse`.

## Serialization

```ts
type SerializerContext = {
request: RequestConfig;
requestId: string;
};

type SerializedBody = {
body: BodyInit;
contentType?: string;
};

type BodySerializer = (
body: unknown,
ctx: SerializerContext,
) => SerializedBody | Promise<SerializedBody>;

type ResponseParser = (response: Response, ctx: SerializerContext) => unknown | Promise<unknown>;
```

## Hooks
Expand All @@ -64,27 +166,93 @@ type ResponseValidator<TData = unknown> = (data: TData) => boolean | void | Prom
- `onError`
- `onRetry`

Each accepts a single function or an array of functions.

```ts
type RetrySource = 'backoff' | 'retry-after';
```

## Telemetry

```ts
type TelemetryExporter = {
onRequestStart?(ctx: BeforeRequestContext): void | Promise<void>;
onRequestSuccess?(ctx: AfterResponseContext): void | Promise<void>;
onRequestError?(ctx: ErrorContext): void | Promise<void>;
onRequestRetry?(ctx: RetryContext): void | Promise<void>;
};
```

## Auth

```ts
type AuthContext = {
request: RequestConfig;
url: URL;
headers: Record<string, string>;
};

type AuthProvider = (ctx: AuthContext) => void | Promise<void>;
```

Strategies: `bearer`, `apiKey`, `custom`.

## Errors

- `HttpError`
- `NetworkError`
- `TimeoutError`
- `ValidationError`
- `RequestAbortedError`
- `DfsyncError` — base class, carries `code`, optional `cause`, and `requestId` / `attempt` / `durationMs`
- `HttpError` — non-2xx responses
- `NetworkError` — network failures
- `TimeoutError` — request timed out
- `ValidationError` — response validation failed, exposes optional `issues`
- `RequestAbortedError` — request was cancelled

## Exported types

- `AuthConfig`
Client and requests:

- `Client`
- `ClientConfig`
- `RequestConfig`
- `RequestOptions`
- `RequestMethod`

Retry:

- `RetryConfig`
- `RetryCondition`
- `RetryBackoff`
- `RetryPredicate`
- `RetryPredicateContext`

Validation:

- `ResponseValidator`
- `ValidationAdapter`
- `ValidationResult`

Serialization:

- `BodySerializer`
- `ResponseParser`
- `SerializedBody`
- `SerializerContext`

Auth:

- `AuthConfig`
- `AuthContext`
- `AuthProvider`

Hooks and telemetry:

- `HooksConfig`
- `HookBeforeRequest`
- `HookAfterResponse`
- `HookOnError`
- `HookOnRetry`
- `BeforeRequestContext`
- `AfterResponseContext`
- `ErrorContext`
- `RetryContext`
- `HooksConfig`
- `RequestConfig`
- `RequestOptions`
- `RetrySource`
- `TelemetryExporter`
28 changes: 23 additions & 5 deletions docs/client/v1/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ const client = createClient({
});
```

The `apply` function is the exported `AuthProvider` interface, so you can define it separately and reuse it across clients:

```ts
import type { AuthProvider } from '@dfsync/client';

const serviceAuth: AuthProvider = ({ headers }) => {
headers['x-service-name'] = 'billing-worker';
};
```

See **Extensibility** for the full interface.

## Auth context

Custom auth receives:
Expand Down Expand Up @@ -206,16 +218,22 @@ type ApiKeyAuthConfig = {
name?: string;
};

type AuthContext = {
request: RequestConfig;
url: URL;
headers: Record<string, string>;
};

type AuthProvider = (ctx: AuthContext) => void | Promise<void>;

type CustomAuthConfig = {
type: 'custom';
apply: (ctx: {
request: RequestConfig;
url: URL;
headers: Record<string, string>;
}) => void | Promise<void>;
apply: AuthProvider;
};
```

`AuthContext` and `AuthProvider` are exported from the package.

## Common use cases

- internal service authentication
Expand Down
Loading
Loading