# @coda/api-common

Shared RPC infrastructure for all ConnectRPC API client packages. Every `@coda/*-api` client is built on this foundation.

## Installation

This is a private workspace package — consumed via the pnpm workspace link:

```jsonc
// package.json
{ "dependencies": { "@coda/api-common": "workspace:*" } }
```

## Overview

This package provides the building blocks for making ConnectRPC calls with consistent timeout, retry, and error-handling behaviour:

- **`callRpc`** — execute a unary RPC with automatic timeout, retry, and error mapping
- **`RpcResult<T>`** — discriminated union result type (replaces `T | null`)
- **`createTransport`** — create a Connect (HTTP/1.1 JSON) or gRPC (HTTP/2) transport
- **`createClientSet`** — build a typed map of clients from URL configs
- **`forwardHeaders`** — interceptor that propagates request context (request ID, client IP)
- **`buildOutboundHeaders`** — construct standard headers from a service identity and request context
- **Pagination types** — `PageRequest` / `PageInfo` proto-generated types shared by all services

## Usage

### Making RPC calls

Domain clients use `callRpc` internally. You rarely call it directly, but understanding it helps when reading client code:

```ts
import { callRpc, rpcOk, isRpcOk, type RpcResult } from "@coda/api-common";

const result: RpcResult<MyResponse> = await callRpc(
  "myMethod",
  () => client,
  (c, signal) => c.myMethod(req, { signal }),
  { timeoutMs: 500, retry: { attempts: 3 } },
);

if (isRpcOk(result)) {
  console.log(result.data);
} else {
  console.error(result.reason); // "timeout" | "unavailable" | "not_found" | ...
}
```

### Creating clients

Use `createClientSet` to eagerly build a typed map of clients from config:

```ts
import { createClientSet } from "@coda/api-common";
import { AccessClient } from "@coda/admin-api";
import { SearchClient } from "@coda/search-api";

const clients = createClientSet(
  {
    access: { url: config.platformUrl, create: (c) => new AccessClient(c) },
    search: { url: config.searchUrl, create: (c) => new SearchClient(c) },
  },
  { interceptors: [requestIdInterceptor] },
);

clients.access; // AccessClient | null
clients.search; // SearchClient | null
```

### Forwarding request context

```ts
import { forwardHeaders, buildOutboundHeaders } from "@coda/api-common";

const interceptor = forwardHeaders(() =>
  buildOutboundHeaders(
    { name: "my-service", version: "1.0.0" },
    requestContext.getStore(),
  ),
);
```

## Exports

### Types

| Type                    | Description                                                               |
| ----------------------- | ------------------------------------------------------------------------- |
| `RpcResult<T>`          | `{ ok: true, data: T } \| { ok: false, reason: RpcFailReason }`           |
| `RpcFailReason`         | `"timeout" \| "unavailable" \| "not_found" \| "permission_denied" \| ...` |
| `BaseClientConfig`      | Standard config for all domain client constructors                        |
| `CallRpcOptions`        | Per-call timeout, retry, and error hook config                            |
| `RetryConfig`           | Retry attempts and back-off strategy                                      |
| `TransportConfig`       | URL + wire format + interceptors for `createTransport`                    |
| `ClientConfig`          | Per-client URL + interceptors for `createClientSet`                       |
| `ClientEntry<T>`        | Factory entry for `createClientSet`                                       |
| `SharedClientConfig`    | Cross-client interceptors and error hook                                  |
| `ServiceRequestContext` | Per-request trace context (request ID, client IP)                         |
| `ServiceIdentity`       | Service name + version for outbound headers                               |
| `PageRequest`           | Proto pagination request (pageSize, after, before)                        |
| `PageInfo`              | Proto pagination response metadata                                        |

### Functions

| Function                               | Description                                     |
| -------------------------------------- | ----------------------------------------------- |
| `callRpc(method, getClient, fn, opts)` | Execute a unary RPC with timeout + retry        |
| `rpcOk(data)`                          | Construct a success `RpcResult`                 |
| `rpcFail(reason, error?)`              | Construct a failure `RpcResult`                 |
| `isRpcOk(result)`                      | Type guard narrowing to `{ ok: true, data: T }` |
| `createTransport(config)`              | Create a Connect or gRPC transport              |
| `createClientSet(entries, shared?)`    | Build a typed client map from URL configs       |
| `forwardHeaders(getHeaders)`           | Interceptor that injects dynamic headers        |
| `buildOutboundHeaders(service, ctx)`   | Build standard outbound header map              |
| `mapConnectError(err)`                 | Map raw error to `RpcFailReason`                |
| `isRetryableReason(reason)`            | Whether a failure reason is worth retrying      |

## Proto sources

`proto/common/` — shared pagination `.proto` files.

## Generated code

`gen/` — auto-generated TypeScript. Do not edit manually.

## Regenerate

```bash
pnpm buf:generate
```

Requires [Buf CLI](https://buf.build/). Generated output is committed to the repo.
