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

317
node_modules/@trpc/client/skills/client-setup/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,317 @@
---
name: client-setup
description: >
Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure
link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer
on links (not client constructor). Infer types with inferRouterInputs and
inferRouterOutputs. AbortController signal support. TRPCClientError typing.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/client/overview.md
- www/docs/client/vanilla/overview.md
- www/docs/client/vanilla/setup.mdx
- www/docs/client/vanilla/infer-types.md
- www/docs/client/headers.md
- packages/client/src/internals/TRPCUntypedClient.ts
---
# tRPC -- Client Setup
## Setup
```ts
// server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
user: t.router({
byId: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => ({ id: input.id, name: 'Bilbo' })),
create: t.procedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => ({ id: '1', ...input })),
}),
});
export type AppRouter = typeof appRouter;
```
```ts
// client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
const user = await client.user.byId.query({ id: '1' });
const created = await client.user.create.mutate({ name: 'Frodo' });
```
## Core Patterns
### Dynamic Auth Headers
```ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
let token = '';
export function setToken(newToken: string) {
token = newToken;
}
export const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
headers() {
return {
Authorization: token ? `Bearer ${token}` : '',
};
},
}),
],
});
```
The `headers` callback is invoked on every HTTP request, so token changes take effect immediately.
### Inferring Procedure Input and Output Types
```ts
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type UserCreateInput = RouterInput['user']['create'];
type UserByIdOutput = RouterOutput['user']['byId'];
```
### Aborting Requests with AbortController
```ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
const ac = new AbortController();
const query = client.user.byId.query({ id: '1' }, { signal: ac.signal });
ac.abort();
```
### Typed Error Handling
```ts
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from './server';
function isTRPCClientError(
cause: unknown,
): cause is TRPCClientError<AppRouter> {
return cause instanceof TRPCClientError;
}
try {
await client.user.byId.query({ id: '1' });
} catch (cause) {
if (isTRPCClientError(cause)) {
console.log('tRPC error code:', cause.data?.code);
}
}
```
## Common Mistakes
### [CRITICAL] Missing AppRouter type parameter on createTRPCClient
Wrong:
```ts
const client = createTRPCClient({ links: [httpBatchLink({ url })] });
```
Correct:
```ts
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] });
```
Without the type parameter, all procedure calls return `any` and type safety is completely lost.
Source: www/docs/client/vanilla/setup.mdx
### [CRITICAL] Transformer goes on individual links, not createTRPCClient
In v11, the `transformer` option is on individual terminating links, not the client constructor:
```ts
import superjson from 'superjson';
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
transformer: superjson,
}),
],
});
```
In v11, the `transformer` option was moved from the client constructor to individual terminating links. Passing it to `createTRPCClient` throws a TypeError.
Source: packages/client/src/internals/TRPCUntypedClient.ts
### [CRITICAL] Transformer on server but not on client links
Wrong:
```ts
// Server: initTRPC.create({ transformer: superjson })
// Client:
httpBatchLink({ url: 'http://localhost:3000' });
```
Correct:
```ts
// Server: initTRPC.create({ transformer: superjson })
// Client:
httpBatchLink({ url: 'http://localhost:3000', transformer: superjson });
```
If the server uses a transformer, every terminating link on the client must also specify that transformer. Mismatch causes "Unable to transform response" errors.
Source: https://github.com/trpc/trpc/issues/7083
### [CRITICAL] Using import instead of import type for AppRouter
Wrong:
```ts
import { AppRouter } from '../server/router';
```
Correct:
```ts
import type { AppRouter } from '../server/router';
```
A non-type import pulls the entire server bundle into the client. Use `import type` so it is erased at build time.
Source: www/docs/client/vanilla/setup.mdx
### [CRITICAL] Importing appRouter value to derive type in client
Wrong:
```ts
import { appRouter } from '../server/router';
type AppRouter = typeof appRouter;
```
Correct:
```ts
// In server: export type AppRouter = typeof appRouter;
// In client:
import type { AppRouter } from '../server/router';
```
Importing the `appRouter` value (not just the type) bundles the entire server into the client, shipping server code to the browser.
Source: www/docs/server/routers.md
### [CRITICAL] Using type assertions to bypass AppRouter import errors
Wrong:
```ts
const client = createTRPCClient<any>({ links: [httpBatchLink({ url })] });
```
Correct:
```ts
// Fix the import path or monorepo configuration
import type { AppRouter } from '@myorg/api-types';
const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] });
```
Casting to `any` or manually recreating the router type destroys end-to-end type safety. Fix the import path or monorepo config instead.
Source: www/docs/client/vanilla/setup.mdx
### [CRITICAL] Using createTRPCProxyClient (renamed in v11)
Wrong:
```ts
import { createTRPCProxyClient } from '@trpc/client';
```
Correct:
```ts
import { createTRPCClient } from '@trpc/client';
```
`createTRPCProxyClient` was renamed to `createTRPCClient` in v11.
Source: www/docs/client/vanilla/setup.mdx
### [CRITICAL] Treating tRPC as a REST API
Wrong:
```ts
fetch('/api/trpc/users/123', { method: 'GET' });
```
Correct:
```ts
const user = await client.user.byId.query({ id: '123' });
// Raw equivalent: GET /api/trpc/user.byId?input={"id":"123"}
```
tRPC uses JSON-RPC over HTTP. Procedures are called by dot-separated name with JSON input, not by REST resource paths.
Source: www/docs/client/overview.md
### [HIGH] HTML error page instead of JSON response
If you see `couldn't parse JSON, invalid character '<'`, the tRPC endpoint returned an HTML page (404/503) instead of JSON. This means the `url` in your link config is wrong or infrastructure routing is misconfigured -- it is not a tRPC bug. Verify the URL matches your adapter's mount point.
Source: www/docs/client/vanilla/setup.mdx
## See Also
- `links` -- configure httpBatchLink, httpLink, splitLink, and other link types
- `superjson` -- set up SuperJSON transformer on server and client
- `server-setup` -- define routers, procedures, context, and export AppRouter type
- `react-query-setup` -- use tRPC with TanStack React Query for React applications

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 |

273
node_modules/@trpc/client/skills/superjson/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,273 @@
---
name: superjson
description: >
Configure SuperJSON transformer on both server initTRPC.create({ transformer:
superjson }) and every client terminating link (httpBatchLink, httpLink, wsLink,
httpSubscriptionLink) to support Date, Map, Set, BigInt over the wire. Transformer
must match on both sides. In v11, transformer goes on individual links, not the
client constructor.
type: composition
library: trpc
library_version: '11.15.1'
requires:
- server-setup
- client-setup
sources:
- www/docs/server/data-transformers.md
---
# tRPC -- SuperJSON Transformer
## Setup
### 1. Install superjson
```bash
npm install superjson
```
### 2. Add to initTRPC on the server
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t = initTRPC.create({
transformer: superjson,
});
export const router = t.router;
export const publicProcedure = t.procedure;
```
### 3. Add to every terminating link on the client
```ts
// client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
],
});
```
Now Date, Map, Set, BigInt, RegExp, undefined, and other non-JSON types survive the round trip.
## Core Patterns
### SuperJSON with splitLink and Subscriptions
```ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
false: httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
}),
],
});
```
Every terminating link in every branch must have `transformer: superjson`.
### SuperJSON with wsLink
```ts
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';
const wsClient = createWSClient({
url: 'ws://localhost:3000',
});
const client = createTRPCClient<AppRouter>({
links: [
wsLink<AppRouter>({
client: wsClient,
transformer: superjson,
}),
],
});
```
### Returning Dates from Procedures
```ts
// server
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
const appRouter = router({
getEvent: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
return {
id: input.id,
name: 'Launch Party',
date: new Date('2025-01-01T00:00:00Z'),
};
}),
});
export type AppRouter = typeof appRouter;
```
```ts
// client
const event = await client.getEvent.query({ id: '1' });
console.log(event.date instanceof Date); // true
console.log(event.date.getFullYear()); // 2025
```
Without superjson, `event.date` would be a string like `"2025-01-01T00:00:00.000Z"`.
## Common Mistakes
### [CRITICAL] Transformer on server but missing from client link
Wrong:
```ts
// Server
const t = initTRPC.create({ transformer: superjson });
// Client
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
```
Correct:
```ts
// Server
const t = initTRPC.create({ transformer: superjson });
// Client
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
],
});
```
Server encodes with superjson but client tries to parse raw JSON, causing "Unable to transform response" or garbled data.
Source: www/docs/server/data-transformers.md
### [CRITICAL] Transformer goes on individual links, not createTRPCClient
The `transformer` option is on individual terminating links:
```ts
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
],
});
```
In v11, `transformer` was moved from the client constructor to individual links. Passing it to `createTRPCClient` throws a TypeError.
Source: packages/client/src/internals/TRPCUntypedClient.ts
### [CRITICAL] Transformer on only some terminating links in splitLink
Wrong:
```ts
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
// missing transformer!
}),
false: httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
});
```
Correct:
```ts
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
false: httpBatchLink({
url: 'http://localhost:3000/trpc',
transformer: superjson,
}),
});
```
Every terminating link must have the same transformer. A missing transformer on one branch causes deserialization failures only for operations routed through that branch.
Source: www/docs/server/data-transformers.md
### [HIGH] Using transformer on client but not on server
Wrong:
```ts
// Server -- no transformer
const t = initTRPC.create();
// Client
httpBatchLink({ url, transformer: superjson });
```
Correct:
```ts
// Server
const t = initTRPC.create({ transformer: superjson });
// Client
httpBatchLink({ url, transformer: superjson });
```
The transformer must be configured on both `initTRPC.create()` and every client link. Client-only transformer corrupts the request encoding because the server expects plain JSON.
Source: www/docs/server/data-transformers.md
## See Also
- `client-setup` -- create the tRPC client and configure links
- `links` -- detailed options for each link type including transformer
- `server-setup` -- initTRPC.create() where the server transformer is configured