@trpc/client
Version:
162 lines (110 loc) • 8.48 kB
Markdown
# HTTP Batch Stream Link
> A terminating link that batches tRPC calls and streams each response as soon as it is ready
From https://trpc.io/docs/client/links/httpBatchStreamLink
`httpBatchStreamLink` is a [**terminating link**](https://trpc.io/docs/client/links#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request (equivalent to [`httpBatchLink`](https://trpc.io/docs/client/links/httpBatchLink)), but doesn't wait for all the responses of the batch to be ready and streams the responses as soon as any data is available.
## Options
Options are identical to [`httpBatchLink options`](https://trpc.io/docs/client/links/httpBatchLink#options), with the following addition:
| Option | Type | Default | Description |
| -------------- | ----------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `streamHeader` | `'trpc-accept'` \| `'accept'` | `'trpc-accept'` | Which header to use to signal the server that the client wants a streaming response. `'accept'` uses the standard `Accept` header instead of the custom `trpc-accept` header, which can avoid CORS preflight for cross-origin streaming queries since `Accept` is a CORS-safelisted header. |
## Usage
> All usage and options are identical to [`httpBatchLink`](https://trpc.io/docs/client/links/httpBatchLink).
> [!NOTE]
> If you require the ability to change/set response headers (which includes cookies) from within your procedures, make sure to use `httpBatchLink` instead! This is due to the fact that `httpBatchStreamLink` does not support setting headers once the stream has begun. [Read more](https://trpc.io/docs/client/links/httpBatchLink).
You can import and add the `httpBatchStreamLink` to the `links` array as such:
```ts no-check title="client/index.ts"
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchStreamLink({
url: 'http://localhost:3000',
}),
],
});
```
After that, calls made in the same event loop tick are batched together, so you don't need `Promise.all`, though it's a common way to do it. The code below will produce exactly **one** HTTP request, and the server will call `createContext` **once** for it:
```ts no-check
const somePosts = await Promise.all([
trpc.post.byId.query(1),
trpc.post.byId.query(2),
trpc.post.byId.query(3),
]);
```
Each procedure still runs its own resolver, so whether this makes one database query depends on your data layer. Prisma [batches `findUnique()` calls made in the same tick](https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance#solving-the-n1-problem) into one query, and a [DataLoader](https://github.com/graphql/dataloader) can do the same for other queries. Otherwise, each resolver makes its own query.
## Streaming mode
When batching requests together, the behavior of a regular `httpBatchLink` is to wait for all requests to finish before sending the response. If you want to send responses as soon as they are ready, you can use `httpBatchStreamLink` instead. This is useful for long-running requests.
```ts no-check title="client/index.ts"
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchStreamLink({
url: 'http://localhost:3000',
}),
],
});
```
Compared to a regular `httpBatchLink`, a `httpBatchStreamLink` will:
- Cause the requests to be sent with a `trpc-accept: application/jsonl` header (or `Accept: application/jsonl` when using `streamHeader: 'accept'`)
- Cause the response to be sent with `transfer-encoding: chunked` and `content-type: application/json` by default. You can set the streamed response `content-type` to `application/jsonl`, `application/x-ndjson`, or another value on the server via `initTRPC.create({ jsonl: { contentType: ... } })`.
- Remove the `data` key from the argument object passed to `responseMeta` (because with a streamed response, the headers are sent before the data is available)
## Async generators and deferred promises
You can try this out on the homepage of tRPC.io: [https://trpc.io/?try=minimal#try-it-out](https://trpc.io/?try=minimal#try-it-out)
```ts no-check
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client';
import type { AppRouter } from './server';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchStreamLink({
url: 'http://localhost:3000',
}),
],
});
const iterable = await trpc.examples.iterable.query();
for await (const value of iterable) {
console.log('Iterable:', value);
}
```
## Compatibility (client-side)
### Browsers
Browser support should be identical to [`fetch`](https://caniuse.com/fetch) support.
### Node.js / Deno
For runtimes other than the browser ones, the `fetch` implementation should support streaming, meaning that the response obtained by `await fetch(...)` should have a `body` property of type `ReadableStream<Uint8Array> | NodeJS.ReadableStream`, meaning that:
- either `response.body.getReader` is a function that returns a `ReadableStreamDefaultReader<Uint8Array>` object
- or `response.body` is a `Uint8Array` `Buffer`
This includes support for `undici`, `node-fetch`, native Node.js fetch implementation, and WebAPI fetch implementation (browsers).
### React Native
Receiving the stream relies on the `TextDecoder` and `TextDecoderStream` APIs, which are not available in React Native. It's important to note that if your `TextDecoderStream` polyfill does not automatically polyfill `ReadableStream` and `WritableStream` those will also need to be polyfilled. If you still want to enable streaming, you need to polyfill those.
You will also need to override the default fetch in the `httpBatchStreamLink` configuration options with one that can stream. In the below example we will be using [`react-native-fetch-api`](https://github.com/react-native-community/fetch), which streams text when you pass its `reactNative` option. On Expo, you can pass `fetch` from [`expo/fetch`](https://docs.expo.dev/versions/latest/sdk/expo/) instead, which streams without extra options.
```ts
import { httpBatchStreamLink } from '@trpc/client';
import { fetch } from 'react-native-fetch-api';
httpBatchStreamLink({
fetch: (url, opts) =>
fetch(url, {
...opts,
reactNative: { textStreaming: true },
}),
url: 'http://localhost:3000',
});
```
## Compatibility (server-side)
> [!WARNING] AWS Lambda
> `httpBatchStreamLink` only supported on AWS Lambda when your [infrastructure is set up](https://trpc.io/docs/server/adapters/aws-lambda#aws-lambda-response-streaming-adapter) for streaming responses. If not this Link will simply behave like a regular `httpBatchLink`.
> [!WARNING] Cloudflare Workers
> You need to enable the `ReadableStream` API through a feature flag: [`streams_enable_constructors`](https://developers.cloudflare.com/workers/platform/compatibility-dates#streams-constructors).
## Reference
You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/httpBatchStreamLink.ts)
## Configure JSONL streaming options
When setting up your root config, you can pass in a `jsonl` option to configure ping behavior and the response `content-type` for streamed batch responses.
```ts no-check
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({
jsonl: {
pingMs: 1000,
contentType: 'application/jsonl',
},
});
```
The default `contentType` remains `application/json` for backwards compatibility. Use `application/jsonl` if you want the response header to match the `trpc-accept` / `Accept` header sent by `httpBatchStreamLink`, or choose another value that works better with your tooling.