FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

301
node_modules/@trpc/client/skills/links/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,301 @@
---
name: links
description: >
Configure the tRPC client link chain: httpLink, httpBatchLink,
httpBatchStreamLink, splitLink, loggerLink, wsLink, createWSClient,
httpSubscriptionLink, unstable_localLink, retryLink. Choose the right
terminating link. Route subscriptions via splitLink. Build custom links
for SOA routing. Link options: url, headers, transformer, maxURLLength,
maxItems, connectionParams, EventSource ponyfill.
type: core
library: trpc
library_version: '11.15.1'
requires:
- client-setup
sources:
- www/docs/client/links/overview.md
- www/docs/client/links/httpLink.md
- www/docs/client/links/httpBatchLink.md
- www/docs/client/links/httpBatchStreamLink.md
- www/docs/client/links/splitLink.mdx
- www/docs/client/links/wsLink.md
- www/docs/client/links/httpSubscriptionLink.md
- www/docs/client/links/localLink.mdx
- www/docs/client/links/loggerLink.md
- packages/client/src/links/
---
# tRPC -- Links
## Setup
```ts
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
loggerLink(),
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
```
The `links` array is a chain: non-terminating links (loggerLink, splitLink, retryLink) forward operations; the chain must end with a terminating link (httpBatchLink, httpLink, httpBatchStreamLink, wsLink, httpSubscriptionLink, unstable_localLink).
## Core Patterns
### httpBatchLink -- Batch Multiple Calls into One Request
```ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
maxURLLength: 2083,
maxItems: 10,
}),
],
});
const [post1, post2, post3] = await Promise.all([
client.post.byId.query(1),
client.post.byId.query(2),
client.post.byId.query(3),
]);
```
Concurrent calls are batched into a single HTTP request. Set `maxURLLength` to prevent 414 errors from long URLs.
### splitLink -- Route Subscriptions to SSE
```ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
}),
false: httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
}),
],
});
```
### splitLink -- Disable Batching Per-Request via Context
```ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => Boolean(op.context.skipBatch),
true: httpLink({ url: 'http://localhost:3000/trpc' }),
false: httpBatchLink({ url: 'http://localhost:3000/trpc' }),
}),
],
});
const result = await client.post.byId.query(1, {
context: { skipBatch: true },
});
```
### httpBatchStreamLink -- Stream Responses as They Arrive
```ts
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchStreamLink({
url: 'http://localhost:3000/trpc',
}),
],
});
const iterable = await client.examples.iterable.query();
for await (const value of iterable) {
console.log(value);
}
```
### wsLink -- WebSocket Terminating Link
```ts
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
import type { AppRouter } from './server';
const wsClient = createWSClient({
url: 'ws://localhost:3000',
});
const client = createTRPCClient<AppRouter>({
links: [wsLink<AppRouter>({ client: wsClient })],
});
```
### Custom Link
```ts
import { TRPCLink } from '@trpc/client';
import { observable } from '@trpc/server/observable';
import type { AppRouter } from './server';
export const timingLink: TRPCLink<AppRouter> = () => {
return ({ next, op }) => {
return observable((observer) => {
const start = Date.now();
const unsubscribe = next(op).subscribe({
next(value) {
observer.next(value);
},
error(err) {
console.error(`${op.path} failed in ${Date.now() - start}ms`);
observer.error(err);
},
complete() {
console.log(`${op.path} completed in ${Date.now() - start}ms`);
observer.complete();
},
});
return unsubscribe;
});
};
};
```
## Common Mistakes
### [CRITICAL] No terminating link in the chain
Wrong:
```ts
const client = createTRPCClient<AppRouter>({
links: [loggerLink()],
});
```
Correct:
```ts
const client = createTRPCClient<AppRouter>({
links: [loggerLink(), httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
```
The link chain must end with a terminating link. Without one, tRPC throws "No more links to execute - did you forget to add an ending link?"
Source: packages/client/src/links/internals/createChain.ts
### [CRITICAL] Sending subscriptions through httpLink or httpBatchLink
Wrong:
```ts
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
await client.onMessage.subscribe({});
```
Correct:
```ts
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({ url: 'http://localhost:3000/trpc' }),
false: httpBatchLink({ url: 'http://localhost:3000/trpc' }),
}),
],
});
```
httpLink and httpBatchLink throw on subscription operations. Subscriptions must use httpSubscriptionLink or wsLink, routed via splitLink.
Source: packages/client/src/links/httpLink.ts
### [HIGH] httpBatchLink and httpBatchStreamLink headers callback receives { opList }
`httpBatchLink` and `httpBatchStreamLink` headers callbacks receive `{ opList }` (a `NonEmptyArray<Operation>`), not `{ op }` like `httpLink`. Access per-operation context via `opList[0]?.context`:
```ts
httpBatchLink({
url: 'http://localhost:3000/trpc',
headers({ opList }) {
return { authorization: opList[0]?.context.token };
},
});
```
`httpBatchLink` headers callback receives `{ opList }` (an array of operations)
Source: packages/client/src/links/httpBatchLink.ts
### [MEDIUM] Default batch limits are Infinity
Wrong:
```ts
httpBatchLink({ url: 'http://localhost:3000/trpc' });
```
Correct:
```ts
httpBatchLink({
url: 'http://localhost:3000/trpc',
maxURLLength: 2083,
// should be the same or lower than the server's maxBatchSize
maxItems: 10,
});
```
Both `maxURLLength` and `maxItems` default to `Infinity`, which can cause 413/414 HTTP errors on servers or CDNs with URL length limits. When the server sets `maxBatchSize`, set `maxItems` to the same or lower value so the client auto-splits batches instead of triggering a `400 Bad Request`.
Source: packages/client/src/links/httpBatchLink.ts
### [HIGH] httpBatchStreamLink data loss on stream completion
There is a known race condition where buffered chunks can be lost on normal stream completion. Long streaming responses (e.g., LLM output) may be truncated. If you experience truncated data, switch to `httpBatchLink` for those operations.
Source: https://github.com/trpc/trpc/issues/7209
## References
- [Link options reference](references/link-options.md)
## See Also
- `client-setup` -- create the tRPC client and configure links
- `superjson` -- add transformer to links for Date/Map/Set support
- `subscriptions` -- set up SSE or WebSocket real-time streams
- `non-json-content-types` -- route FormData/binary through splitLink + httpLink
- `service-oriented-architecture` -- build custom routing links for multi-service backends

View File

@@ -0,0 +1,248 @@
# Link Options Reference
## httpLink
Terminating link that sends one tRPC operation per HTTP request.
```ts
import { httpLink } from '@trpc/client';
httpLink({
url: 'http://localhost:3000/trpc',
fetch: customFetch,
transformer: superjson,
headers: { Authorization: 'Bearer token' },
methodOverride: 'POST',
});
```
| Option | Type | Default | Description |
| ---------------- | --------------------------------------------------------------------------------- | -------------- | --------------------------------------------- |
| `url` | `string \| URL` | required | Server endpoint URL |
| `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill |
| `transformer` | `DataTransformerOptions` | none | Data transformer (e.g. superjson) |
| `headers` | `HTTPHeaders \| (opts: { op: Operation }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Static headers object or per-request callback |
| `methodOverride` | `'POST'` | none | Force all requests as POST |
## httpBatchLink
Terminating link that batches multiple operations into a single HTTP request.
```ts
import { httpBatchLink } from '@trpc/client';
httpBatchLink({
url: 'http://localhost:3000/trpc',
maxURLLength: 2083,
maxItems: 10,
headers({ opList }) {
return { Authorization: `Bearer ${opList[0]?.context.token}` };
},
transformer: superjson,
});
```
| Option | Type | Default | Description |
| ---------------- | --------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------- |
| `url` | `string \| URL` | required | Server endpoint URL |
| `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill |
| `transformer` | `DataTransformerOptions` | none | Data transformer |
| `headers` | `HTTPHeaders \| (opts: { opList: Operation[] }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Headers callback receives `opList` (array), not `op` |
| `maxURLLength` | `number` | `Infinity` | Split batch if URL exceeds this length |
| `maxItems` | `number` | `Infinity` | Maximum operations per batch |
| `methodOverride` | `'POST'` | none | Force all requests as POST |
## httpBatchStreamLink
Terminating link similar to httpBatchLink but streams responses as they arrive instead of waiting for all to complete.
```ts
import { httpBatchStreamLink } from '@trpc/client';
httpBatchStreamLink({
url: 'http://localhost:3000/trpc',
maxURLLength: 2083,
maxItems: 10,
transformer: superjson,
streamHeader: 'accept',
});
```
| Option | Type | Default | Description |
| --------------------------- | --------------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
| All `httpBatchLink` options | | | Inherits all httpBatchLink options |
| `streamHeader` | `'trpc-accept' \| 'accept'` | `'trpc-accept'` | Header used to signal streaming. Use `'accept'` to avoid CORS preflight on cross-origin requests. |
Sends `trpc-accept: application/jsonl` (or `Accept: application/jsonl`). Response arrives as `transfer-encoding: chunked` with `content-type: application/jsonl`. Cannot set response headers (including cookies) after stream begins.
## splitLink
Non-terminating link that branches the link chain based on a condition.
```ts
import {
httpBatchLink,
httpLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({ url }),
false: httpBatchLink({ url }),
});
```
| Option | Type | Default | Description |
| ----------- | ---------------------------- | -------- | ------------------------------------------------------------- |
| `condition` | `(op: Operation) => boolean` | required | Route predicate |
| `true` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=true. Must include a terminating link. |
| `false` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=false. Must include a terminating link. |
Each branch creates its own sub-chain, so both branches need a terminating link.
## loggerLink
Non-terminating link that logs operations to the console.
```ts
import { loggerLink } from '@trpc/client';
loggerLink({
enabled: (opts) =>
(process.env.NODE_ENV === 'development' && typeof window !== 'undefined') ||
(opts.direction === 'down' && opts.result instanceof Error),
colorMode: 'ansi',
});
```
| Option | Type | Default | Description |
| ------------- | -------------------------------------------------------------------- | ------------------------------------ | -------------------------------- |
| `enabled` | `(opts: { direction: 'up' \| 'down'; result?: unknown }) => boolean` | `() => true` | Control when logging is active |
| `logger` | `(opts: LoggerOpts) => void` | built-in pretty logger | Custom log function |
| `console` | `{ log: Function; error: Function }` | `globalThis.console` | Console implementation |
| `colorMode` | `'ansi' \| 'css' \| 'none'` | `'css'` in browser, `'ansi'` in Node | Color output mode |
| `withContext` | `boolean` | `false` (true if css) | Include operation context in log |
## retryLink
Non-terminating link that retries failed operations.
```ts
import { retryLink } from '@trpc/client';
retryLink({
retry(opts) {
if (opts.error.data?.code === 'INTERNAL_SERVER_ERROR') {
return opts.attempts <= 3;
}
return false;
},
retryDelayMs: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
});
```
| Option | Type | Default | Description |
| -------------- | -------------------------------------------- | --------- | --------------------------- |
| `retry` | `(opts: { op, error, attempts }) => boolean` | required | Return true to retry |
| `retryDelayMs` | `(attempt: number) => number` | `() => 0` | Delay between retries in ms |
When used with subscriptions that use `tracked()`, automatically includes the last known event ID on retry.
## wsLink
Terminating link for WebSocket connections. Requires a `TRPCWebSocketClient`.
```ts
import { createWSClient, wsLink } from '@trpc/client';
const wsClient = createWSClient({
url: 'ws://localhost:3000',
connectionParams: () => ({ token: 'supersecret' }),
lazy: { enabled: true, closeMs: 10_000 },
keepAlive: { enabled: true, intervalMs: 5_000, pongTimeoutMs: 1_000 },
});
wsLink<AppRouter>({
client: wsClient,
transformer: superjson,
});
```
### wsLink Options
| Option | Type | Default | Description |
| ------------- | ------------------------ | -------- | ------------------------------------ |
| `client` | `TRPCWebSocketClient` | required | WebSocket client from createWSClient |
| `transformer` | `DataTransformerOptions` | none | Data transformer |
### createWSClient Options
| Option | Type | Default | Description |
| ------------------------- | ---------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- |
| `url` | `string \| (() => MaybePromise<string>)` | required | WebSocket server URL |
| `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<Record<string, string> \| null>)` | `null` | Auth params sent as first message, available in `createContext()` |
| `WebSocket` | `typeof WebSocket` | global `WebSocket` | WebSocket ponyfill |
| `retryDelayMs` | `(attemptIndex: number) => number` | exponential backoff | Reconnection delay |
| `onOpen` | `() => void` | none | Connection opened callback |
| `onError` | `(evt?: Event) => void` | none | Connection error callback |
| `onClose` | `(cause?: { code?: number }) => void` | none | Connection closed callback |
| `lazy.enabled` | `boolean` | `false` | Close WS after inactivity |
| `lazy.closeMs` | `number` | `0` | Idle timeout before closing |
| `keepAlive.enabled` | `boolean` | `false` | Send ping messages |
| `keepAlive.intervalMs` | `number` | `5000` | Ping interval |
| `keepAlive.pongTimeoutMs` | `number` | `1000` | Close if no pong within this time |
## httpSubscriptionLink
Terminating link for Server-Sent Events (SSE) subscriptions.
```ts
import { httpSubscriptionLink } from '@trpc/client';
import { EventSourcePolyfill } from 'event-source-polyfill';
httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
connectionParams: async () => ({ token: 'supersecret' }),
transformer: superjson,
EventSource: EventSourcePolyfill,
eventSourceOptions: async ({ op }) => ({
headers: {
authorization: 'Bearer token',
},
}),
});
```
| Option | Type | Default | Description |
| -------------------- | ------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------- |
| `url` | `string \| (() => string \| Promise<string>)` | required | Server endpoint URL |
| `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<...>)` | none | Serialized as URL query param |
| `transformer` | `DataTransformerOptions` | none | Data transformer |
| `EventSource` | EventSource constructor | global `EventSource` | EventSource ponyfill for custom headers |
| `eventSourceOptions` | `EventSourceInit \| ((opts: { op }) => EventSourceInit \| Promise<EventSourceInit>)` | none | Options passed to EventSource constructor |
For cross-domain cookies, use `eventSourceOptions: () => ({ withCredentials: true })`.
## unstable_localLink
Terminating link for direct procedure calls without HTTP. Useful for testing and server-side usage.
```ts
import { unstable_localLink } from '@trpc/client';
import { appRouter } from './server';
unstable_localLink({
router: appRouter,
createContext: async () => ({ db: prisma }),
onError: (opts) => console.error('Error:', opts.error),
});
```
| Option | Type | Default | Description |
| --------------- | ------------------------------------- | -------- | ------------------------ |
| `router` | `AnyRouter` | required | tRPC router instance |
| `createContext` | `() => Promise<Context>` | required | Context factory per call |
| `onError` | `(opts: ErrorHandlerOptions) => void` | none | Error handler |
| `transformer` | `DataTransformerOptions` | none | Data transformer |