Skip to content

Commit 91ac36a

Browse files
authored
feat(standard-server): send initial comment in event stream to flush response headers immediately (#1204)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Configurable initial event comment (enable/disable + custom text). * **Bug Fixes** * Streaming/SSE responses now emit an initial keep‑alive heartbeat line before event data. * **Documentation** * Added HTTP Event Iterator configuration docs; removed several client/OpenAPI/RPC keep‑alive sections. * **Tests** * Expanded tests covering initial-comment and keep‑alive sequencing, timing, and stream consumption. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8dab177 commit 91ac36a

File tree

14 files changed

+219
-113
lines changed

14 files changed

+219
-113
lines changed

apps/content/docs/adapters/http.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,52 @@ The `link` can be any supported oRPC link, such as [RPCLink](/docs/client/rpc-li
247247
::: info
248248
This only shows how to configure the http link. For full client examples, see [Client-Side Clients](/docs/client/client-side).
249249
:::
250+
251+
## Event Iterator Options
252+
253+
HTTP adapters provide reliability features for streaming [Event Iterators](/docs/event-iterator):
254+
255+
```ts
256+
const handler = new RPCHandler(router, {
257+
eventIteratorInitialCommentEnabled: true,
258+
eventIteratorInitialComment: 'start',
259+
eventIteratorKeepAliveEnabled: true,
260+
eventIteratorKeepAliveInterval: 5000,
261+
eventIteratorKeepAliveComment: '',
262+
})
263+
264+
const link = new OpenAPILink({
265+
eventIteratorInitialCommentEnabled: true,
266+
eventIteratorInitialComment: 'start',
267+
eventIteratorKeepAliveEnabled: true,
268+
eventIteratorKeepAliveInterval: 5000,
269+
eventIteratorKeepAliveComment: '',
270+
})
271+
```
272+
273+
::: info
274+
These options are available for HTTP-based handlers and links only.
275+
:::
276+
277+
::: warning
278+
Link options apply when streaming from **client to server**, not server to client (as with handlers). In most cases, you don't need to configure these on the link.
279+
:::
280+
281+
### Initial Comment
282+
283+
Sends an initial comment immediately when the stream starts to flush response headers early. This allows the receiving side to establish the connection without waiting for the first event.
284+
285+
| Option | Default | Description |
286+
| ------------------------------------ | ------- | ------------------------------ |
287+
| `eventIteratorInitialCommentEnabled` | `true` | Enable/disable initial comment |
288+
| `eventIteratorInitialComment` | `''` | Custom comment content |
289+
290+
### Keep-Alive Comments
291+
292+
Sends periodic comments during inactivity to prevent connection timeouts.
293+
294+
| Option | Default | Description |
295+
| -------------------------------- | ------- | ---------------------------------- |
296+
| `eventIteratorKeepAliveEnabled` | `true` | Enable/disable keep-alive comments |
297+
| `eventIteratorKeepAliveInterval` | `5000` | Interval in milliseconds |
298+
| `eventIteratorKeepAliveComment` | `''` | Custom comment content |

apps/content/docs/client/rpc-link.md

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,6 @@ const link = new RPCLink({
153153

154154
Unlike traditional SSE, the [Event Iterator](/docs/event-iterator) does not automatically retry on error. To enable automatic retries, refer to the [Client Retry Plugin](/docs/plugins/client-retry).
155155

156-
## Event Iterator Keep Alive
157-
158-
:::warning
159-
These options for sending [Event Iterator](/docs/event-iterator) from **client to the server**, not from **the server to client** as used in [RPCHandler Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive) or [OpenAPIHandler Event Iterator Keep Alive](/docs/openapi/openapi-handler#event-iterator-keep-alive).
160-
161-
**In 99% of cases, you don't need to configure these options.**
162-
:::
163-
164-
To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCLink` periodically sends a ping comment to the server. You can configure this behavior using the following options:
165-
166-
- `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings.
167-
- `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds).
168-
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping messages.
169-
170-
```ts
171-
const link = new RPCLink({
172-
eventIteratorKeepAliveEnabled: true,
173-
eventIteratorKeepAliveInterval: 5000, // 5 seconds
174-
eventIteratorKeepAliveComment: '',
175-
})
176-
```
177-
178156
## Lifecycle
179157

180158
```mermaid

apps/content/docs/openapi/client/openapi-link.md

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -154,28 +154,6 @@ const link = new OpenAPILink({
154154

155155
Unlike traditional SSE, the [Event Iterator](/docs/event-iterator) does not automatically retry on error. To enable automatic retries, refer to the [Client Retry Plugin](/docs/plugins/client-retry).
156156

157-
## Event Iterator Keep Alive
158-
159-
:::warning
160-
These options for sending [Event Iterator](/docs/event-iterator) from **client to the server**, not from **the server to client** as used in [RPCHandler Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive) or [OpenAPIHandler Event Iterator Keep Alive](/docs/openapi/openapi-handler#event-iterator-keep-alive).
161-
162-
**In 99% of cases, you don't need to configure these options.**
163-
:::
164-
165-
To keep [Event Iterator](/docs/event-iterator) connections alive, `OpenAPILink` periodically sends a ping comment to the server. You can configure this behavior using the following options:
166-
167-
- `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings.
168-
- `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds).
169-
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping messages.
170-
171-
```ts
172-
const link = new OpenAPILink({
173-
eventIteratorKeepAliveEnabled: true,
174-
eventIteratorKeepAliveInterval: 5000, // 5 seconds
175-
eventIteratorKeepAliveComment: '',
176-
})
177-
```
178-
179157
## Lifecycle
180158

181159
The `OpenAPILink` follows the same lifecycle as the [RPCLink Lifecycle](/docs/client/rpc-link#lifecycle), ensuring consistent behavior across different link types.

apps/content/docs/openapi/openapi-handler.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -102,22 +102,6 @@ const handler = new OpenAPIHandler(router, {
102102
})
103103
```
104104

105-
## Event Iterator Keep Alive
106-
107-
To keep [Event Iterator](/docs/event-iterator) connections alive, `OpenAPIHandler` periodically sends a ping comment to the client. You can configure this behavior using the following options:
108-
109-
- `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings.
110-
- `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds).
111-
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments.
112-
113-
```ts
114-
const handler = new OpenAPIHandler(router, {
115-
eventIteratorKeepAliveEnabled: true,
116-
eventIteratorKeepAliveInterval: 5000, // 5 seconds
117-
eventIteratorKeepAliveComment: '',
118-
})
119-
```
120-
121105
## Lifecycle
122106

123107
The `OpenAPIHandler` follows the same lifecycle as the [RPCHandler Lifecycle](/docs/rpc-handler#lifecycle), ensuring consistent behavior across different handler types.

apps/content/docs/rpc-handler.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,6 @@ const handler = new RPCHandler(router, {
8383
})
8484
```
8585

86-
## Event Iterator Keep Alive
87-
88-
To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCHandler` periodically sends a ping comment to the client. You can configure this behavior using the following options:
89-
90-
- `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings.
91-
- `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds).
92-
- `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments.
93-
94-
```ts
95-
const handler = new RPCHandler(router, {
96-
eventIteratorKeepAliveEnabled: true,
97-
eventIteratorKeepAliveInterval: 5000, // 5 seconds
98-
eventIteratorKeepAliveComment: '',
99-
})
100-
```
101-
10286
## Default Plugins
10387

10488
`RPCHandler` automatically enables **essential plugins** for security reasons.

packages/standard-server-aws-lambda/src/response.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ describe('sendStandardResponse', () => {
130130
})
131131

132132
expect(chunks).toEqual([
133+
Buffer.from(': \n\n'),
133134
Buffer.from('event: message\ndata: "foo"\n\n'),
134135
Buffer.from('event: message\ndata: "bar"\n\n'),
135136
])
@@ -175,6 +176,7 @@ describe('sendStandardResponse', () => {
175176
await new Promise(r => setTimeout(r, 110))
176177

177178
expect(chunks).toEqual([
179+
Buffer.from(': \n\n'),
178180
Buffer.from('event: message\ndata: 1\n\n'),
179181
Buffer.from('event: message\ndata: 2\n\n'),
180182
])
@@ -229,6 +231,7 @@ describe('sendStandardResponse', () => {
229231
await new Promise(r => setTimeout(r, 110))
230232

231233
expect(chunks).toEqual([
234+
Buffer.from(': \n\n'),
232235
Buffer.from('event: message\ndata: 1\n\n'),
233236
Buffer.from('event: message\ndata: 2\n\n'),
234237
])

packages/standard-server-fastify/src/response.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('sendStandardResponse', () => {
179179
'x-custom-header': 'custom-value',
180180
})
181181

182-
expect(res.text).toEqual('event: message\ndata: "foo"\n\nevent: message\ndata: "bar"\n\nevent: done\ndata: "baz"\n\n')
182+
expect(res.text).toEqual(': \n\nevent: message\ndata: "foo"\n\nevent: message\ndata: "bar"\n\nevent: done\ndata: "baz"\n\n')
183183
})
184184

185185
describe('edge case', () => {

packages/standard-server-fetch/src/body.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ describe('toFetchBody', () => {
296296

297297
const reader = (body as ReadableStream).pipeThrough(new TextDecoderStream()).getReader()
298298

299+
expect(await reader.read()).toEqual({ done: false, value: ': \n\n' })
299300
expect(await reader.read()).toEqual({ done: false, value: 'event: message\ndata: 123\n\n' })
300301
expect(await reader.read()).toEqual({ done: false, value: 'event: done\ndata: 456\n\n' })
301302
})

0 commit comments

Comments
 (0)