ts-procedures 5.8.0 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1052
- package/agent_config/claude-code/skills/guide/api-reference.md +17 -14
- package/agent_config/claude-code/skills/guide/patterns.md +3 -1
- package/agent_config/copilot/copilot-instructions.md +6 -5
- package/agent_config/cursor/cursorrules +6 -5
- package/build/codegen/bin/cli.js +13 -10
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +18 -2
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/emit-scope.js +16 -6
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +166 -0
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/docs/ai-agent-setup.md +61 -0
- package/docs/client-and-codegen.md +193 -0
- package/docs/core.md +473 -0
- package/docs/http-integrations.md +183 -0
- package/docs/streaming.md +199 -0
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +2833 -0
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +632 -0
- package/package.json +6 -1
- package/src/implementations/http/README.md +324 -0
- package/src/implementations/http/express-rpc/README.md +281 -0
- package/src/implementations/http/hono-rpc/README.md +358 -0
- package/src/implementations/http/hono-stream/README.md +525 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# Hono Stream Implementation
|
|
2
|
+
|
|
3
|
+
HTTP streaming and SSE endpoints for streaming procedures created with `CreateStream`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`HonoStreamAppBuilder` provides a builder pattern for creating streaming HTTP endpoints in Hono. It handles `AsyncGenerator` handlers and supports both Server-Sent Events (SSE) and plain text streaming modes.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Requires `hono` as a peer dependency:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install hono
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Procedures } from 'ts-procedures'
|
|
21
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
22
|
+
|
|
23
|
+
// Define your context and config types
|
|
24
|
+
type StreamContext = { userId: string }
|
|
25
|
+
interface RPCConfig {
|
|
26
|
+
scope: string | string[]
|
|
27
|
+
version: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create a procedures factory
|
|
31
|
+
const StreamRPC = Procedures<StreamContext, RPCConfig>()
|
|
32
|
+
|
|
33
|
+
// Create a streaming procedure — yield domain data directly
|
|
34
|
+
StreamRPC.CreateStream(
|
|
35
|
+
'WatchNotifications',
|
|
36
|
+
{
|
|
37
|
+
scope: ['user', 'notifications'],
|
|
38
|
+
version: 1,
|
|
39
|
+
schema: {
|
|
40
|
+
yieldType: v.object({ id: v.number(), message: v.string() }),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async function* (ctx) {
|
|
44
|
+
for (let i = 1; i <= 10; i++) {
|
|
45
|
+
// Yield domain data directly — matches yieldType schema
|
|
46
|
+
yield { id: i, message: `Notification ${i}` }
|
|
47
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Build the Hono app
|
|
53
|
+
const builder = new HonoStreamAppBuilder()
|
|
54
|
+
.register(StreamRPC, (c) => ({
|
|
55
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
const app = builder.build()
|
|
59
|
+
|
|
60
|
+
// Access documentation
|
|
61
|
+
const docs = builder.docs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## HTTP Methods
|
|
65
|
+
|
|
66
|
+
Both GET and POST methods are supported for each streaming endpoint:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
GET /{pathPrefix}/{scope...}/{procedureName}/{version}?param1=value1
|
|
70
|
+
POST /{pathPrefix}/{scope...}/{procedureName}/{version} (JSON body)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **GET**: For EventSource/SSE clients, params passed via query string
|
|
74
|
+
- **POST**: For clients needing complex JSON body params
|
|
75
|
+
|
|
76
|
+
## Yielding in SSE Mode
|
|
77
|
+
|
|
78
|
+
Procedures yield **domain data directly** — the same shape as your `yieldType` schema. `HonoStreamAppBuilder` handles the SSE envelope (`event:`, `id:`, `data:`) automatically.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
async function* (ctx) {
|
|
82
|
+
// Domain objects are JSON.stringify'd into the SSE data: field
|
|
83
|
+
yield { count: 1 }
|
|
84
|
+
|
|
85
|
+
// Strings are passed through as-is (no double-stringify)
|
|
86
|
+
yield 'raw string value'
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
By default, SSE events use the procedure name as the `event:` field and auto-increment the `id:` field.
|
|
91
|
+
|
|
92
|
+
### `sse()` Helper
|
|
93
|
+
|
|
94
|
+
To override SSE metadata (event name, id, retry) for individual yields, use the `sse()` helper:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { sse } from 'ts-procedures/hono-stream'
|
|
98
|
+
|
|
99
|
+
async function* (ctx) {
|
|
100
|
+
// Custom event name
|
|
101
|
+
yield sse({ type: 'heartbeat' }, { event: 'ping' })
|
|
102
|
+
|
|
103
|
+
// Custom id
|
|
104
|
+
yield sse({ msg: 'hello' }, { id: 'msg-001' })
|
|
105
|
+
|
|
106
|
+
// All SSE options
|
|
107
|
+
yield sse({ done: true }, { event: 'complete', id: 'final', retry: 5000 })
|
|
108
|
+
|
|
109
|
+
// sse() with no options — same as plain yield (uses defaults)
|
|
110
|
+
yield sse({ count: 1 })
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`sse()` attaches metadata via a `WeakMap` side-channel, so:
|
|
115
|
+
- The domain object passes through core validation (`validateYields`) unmodified
|
|
116
|
+
- `JSON.stringify` only sees domain properties
|
|
117
|
+
- AJV's `removeAdditional: true` doesn't affect the metadata
|
|
118
|
+
|
|
119
|
+
> **Note:** `sse()` requires object values (used as `WeakMap` keys). Strings and other primitives cannot carry SSE metadata — they use the default event name (procedure name) and auto-incremented id.
|
|
120
|
+
|
|
121
|
+
**Text mode is unaffected** — both plain objects and `sse()`-tagged objects are JSON-stringified identically. SSE metadata is ignored in text mode.
|
|
122
|
+
|
|
123
|
+
## Stream Modes
|
|
124
|
+
|
|
125
|
+
### SSE Mode (default)
|
|
126
|
+
|
|
127
|
+
Returns `text/event-stream` content type with SSE-formatted messages:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Response format:
|
|
134
|
+
```
|
|
135
|
+
event: WatchNotifications
|
|
136
|
+
data: {"id":1,"message":"Notification 1"}
|
|
137
|
+
id: 0
|
|
138
|
+
|
|
139
|
+
event: WatchNotifications
|
|
140
|
+
data: {"id":2,"message":"Notification 2"}
|
|
141
|
+
id: 1
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Client usage:
|
|
145
|
+
```typescript
|
|
146
|
+
const eventSource = new EventSource('/user/notifications/watch-notifications/1')
|
|
147
|
+
// Listen for default event (procedure name)
|
|
148
|
+
eventSource.addEventListener('WatchNotifications', (event) => {
|
|
149
|
+
const data = JSON.parse(event.data)
|
|
150
|
+
console.log(data)
|
|
151
|
+
})
|
|
152
|
+
// Listen for custom events (when using sse() helper)
|
|
153
|
+
eventSource.addEventListener('notification', (event) => {
|
|
154
|
+
const data = JSON.parse(event.data)
|
|
155
|
+
console.log(data)
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Text Mode
|
|
160
|
+
|
|
161
|
+
Returns `text/plain` content type with newline-delimited JSON:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Response format:
|
|
168
|
+
```
|
|
169
|
+
{"key":"value"}
|
|
170
|
+
{"key":"value2"}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Client usage:
|
|
174
|
+
```typescript
|
|
175
|
+
const response = await fetch('/user/notifications/watch-notifications/1')
|
|
176
|
+
const reader = response.body.getReader()
|
|
177
|
+
const decoder = new TextDecoder()
|
|
178
|
+
|
|
179
|
+
while (true) {
|
|
180
|
+
const { done, value } = await reader.read()
|
|
181
|
+
if (done) break
|
|
182
|
+
const lines = decoder.decode(value).split('\n')
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
if (line) console.log(JSON.parse(line))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
### Builder Config
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface HonoStreamAppBuilderConfig<TErrorData = unknown> {
|
|
195
|
+
app?: Hono // Use existing Hono instance
|
|
196
|
+
pathPrefix?: string // Prefix for all routes
|
|
197
|
+
defaultStreamMode?: 'sse' | 'text' // Default: 'sse'
|
|
198
|
+
onRequestStart?: (c: Context) => void
|
|
199
|
+
onRequestEnd?: (c: Context) => void
|
|
200
|
+
onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
201
|
+
onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
202
|
+
|
|
203
|
+
// Error handling callbacks (see Error Handling section)
|
|
204
|
+
onPreStreamError?: (procedure, c, error: ProcedureValidationError | Error) => Response | Promise<Response>
|
|
205
|
+
onMidStreamError?: (procedure, c, error) => MidStreamErrorResult<TErrorData> | undefined
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Per-Factory Options
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
builder.register(factory, context, {
|
|
213
|
+
streamMode: 'text', // Override default mode for this factory
|
|
214
|
+
extendProcedureDoc: ({ base, procedure }) => ({
|
|
215
|
+
summary: 'Custom documentation',
|
|
216
|
+
tags: ['streaming'],
|
|
217
|
+
}),
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Lifecycle Hooks
|
|
222
|
+
|
|
223
|
+
Hooks execute in the following order:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
onRequestStart → [validation] → onStreamStart(proc, c, streamMode) → [yields...] → onStreamEnd(proc, c, streamMode) → onRequestEnd
|
|
227
|
+
↓ ↓
|
|
228
|
+
(validation error) (stream error)
|
|
229
|
+
↓ ↓
|
|
230
|
+
HTTP 400 response error sent in stream
|
|
231
|
+
↓
|
|
232
|
+
onStreamEnd
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`onStreamStart` and `onStreamEnd` receive the resolved `streamMode` (`'sse'` or `'text'`) as their third argument.
|
|
236
|
+
|
|
237
|
+
## Validation
|
|
238
|
+
|
|
239
|
+
### Pre-validation
|
|
240
|
+
|
|
241
|
+
`HonoStreamAppBuilder` validates parameters **before** starting the stream to return proper HTTP error responses. It passes `isPrevalidated: true` internally to skip redundant procedure-level validation. This is transparent to handler authors — the flag is not exposed on the handler context type.
|
|
242
|
+
|
|
243
|
+
1. Params are validated by `HonoStreamAppBuilder` at the HTTP layer
|
|
244
|
+
2. If validation fails, a 400 response is returned (no stream starts)
|
|
245
|
+
3. If validation passes, the procedure handler runs without duplicate validation
|
|
246
|
+
|
|
247
|
+
This design allows for:
|
|
248
|
+
- Clean HTTP error responses (400 status code, JSON body) instead of stream errors
|
|
249
|
+
- No duplicate validation overhead
|
|
250
|
+
- Consistent error handling at the framework level
|
|
251
|
+
|
|
252
|
+
## Error Handling
|
|
253
|
+
|
|
254
|
+
Streaming has two distinct error phases with different semantics:
|
|
255
|
+
|
|
256
|
+
| Phase | When | Response Type | Callback |
|
|
257
|
+
|-------|------|---------------|----------|
|
|
258
|
+
| **Pre-stream** | Validation, auth, context resolution | HTTP Response (400/500) | `onPreStreamError` |
|
|
259
|
+
| **Mid-stream** | Generator throws during iteration | Value written to stream | `onMidStreamError` |
|
|
260
|
+
|
|
261
|
+
### Pre-Stream Errors (`onPreStreamError`)
|
|
262
|
+
|
|
263
|
+
Errors that occur **before** the stream starts (validation failures, context resolution errors). The `error` parameter is typed as `ProcedureValidationError | Error`, allowing `instanceof` narrowing. The return value **IS** used as the HTTP response:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const builder = new HonoStreamAppBuilder({
|
|
267
|
+
onPreStreamError: (procedure, c, error) => {
|
|
268
|
+
if (error instanceof ProcedureValidationError) {
|
|
269
|
+
return c.json({
|
|
270
|
+
error: 'Invalid parameters',
|
|
271
|
+
details: error.errors,
|
|
272
|
+
procedure: procedure.name,
|
|
273
|
+
}, 400)
|
|
274
|
+
}
|
|
275
|
+
return c.json({ error: error.message }, 500)
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Without handler, returns default JSON response:
|
|
280
|
+
// Status: 400 (validation) or 500 (other)
|
|
281
|
+
// Body: { "error": "Validation error for ProcedureName - ..." }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Mid-Stream Errors (`onMidStreamError`)
|
|
285
|
+
|
|
286
|
+
Errors that occur **during** streaming (generator throws). Since the stream is already open, HTTP status cannot change. Return `{ data, closeStream? }` — the `data` value is written as the SSE `data:` field content.
|
|
287
|
+
|
|
288
|
+
To control SSE metadata (event name, id, retry) on the error yield, wrap the data object with `sse()`:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const builder = new HonoStreamAppBuilder({
|
|
292
|
+
onMidStreamError: (procedure, c, error) => {
|
|
293
|
+
return {
|
|
294
|
+
// Data written as the SSE data: field (should match your yieldType schema)
|
|
295
|
+
// Use sse() to attach custom event/id/retry metadata
|
|
296
|
+
data: sse({
|
|
297
|
+
type: 'error',
|
|
298
|
+
code: 'STREAM_ERROR',
|
|
299
|
+
message: error.message,
|
|
300
|
+
retryable: false,
|
|
301
|
+
}, { event: 'error', id: 'err-001' }),
|
|
302
|
+
// Optional: whether to close stream after (default: true)
|
|
303
|
+
closeStream: true,
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Return values:**
|
|
310
|
+
- `{ data, closeStream? }` - Write `data` to stream. Use `sse(data, opts)` for custom SSE event/id/retry
|
|
311
|
+
- `undefined` - Use default behavior: `{ error: message }` with `event: 'error'`
|
|
312
|
+
|
|
313
|
+
Without `sse()`, the event defaults to the procedure name when custom data is provided, or `'error'` when using the default error format. The id auto-increments.
|
|
314
|
+
|
|
315
|
+
### Type-Safe Error Handling with Union Types
|
|
316
|
+
|
|
317
|
+
Use the generic `TErrorData` parameter on `HonoStreamAppBuilder` to constrain the `onMidStreamError` return type:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
type ErrorPayload = {
|
|
321
|
+
type: 'error'
|
|
322
|
+
code: string
|
|
323
|
+
message: string
|
|
324
|
+
retryable: boolean
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// TErrorData constrains what onMidStreamError can return
|
|
328
|
+
const builder = new HonoStreamAppBuilder<ErrorPayload>({
|
|
329
|
+
onMidStreamError: (proc, c, error) => ({
|
|
330
|
+
data: {
|
|
331
|
+
type: 'error',
|
|
332
|
+
code: 'STREAM_ERROR',
|
|
333
|
+
message: error.message,
|
|
334
|
+
retryable: false,
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
For type-safe error handling, define your `yieldType` as a discriminated union:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
StreamRPC.CreateStream('WatchEvents', {
|
|
344
|
+
scope: 'events',
|
|
345
|
+
version: 1,
|
|
346
|
+
schema: {
|
|
347
|
+
params: v.object({ roomId: v.string() }),
|
|
348
|
+
// Union type: clients know exactly what to expect
|
|
349
|
+
yieldType: v.union([
|
|
350
|
+
v.object({
|
|
351
|
+
type: v.literal('event'),
|
|
352
|
+
eventType: v.string(),
|
|
353
|
+
data: v.any()
|
|
354
|
+
}),
|
|
355
|
+
v.object({
|
|
356
|
+
type: v.literal('error'),
|
|
357
|
+
code: v.string(),
|
|
358
|
+
message: v.string(),
|
|
359
|
+
retryable: v.boolean()
|
|
360
|
+
})
|
|
361
|
+
])
|
|
362
|
+
}
|
|
363
|
+
}, async function* (ctx, params) {
|
|
364
|
+
// Yield domain data directly — matches yieldType schema
|
|
365
|
+
yield { type: 'event', eventType: 'user_joined', data: { userId: ctx.userId } }
|
|
366
|
+
// On error, the procedure throws and onMidStreamError handles it
|
|
367
|
+
})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Output (SSE mode):**
|
|
371
|
+
```
|
|
372
|
+
event: WatchEvents
|
|
373
|
+
data: {"type":"event","eventType":"user_joined","data":{"userId":"123"}}
|
|
374
|
+
|
|
375
|
+
event: WatchEvents
|
|
376
|
+
data: {"type":"error","code":"STREAM_ERROR","message":"Connection lost","retryable":false}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Default Error Output
|
|
380
|
+
|
|
381
|
+
Without custom callbacks, errors are written as:
|
|
382
|
+
|
|
383
|
+
**SSE Mode:**
|
|
384
|
+
```
|
|
385
|
+
event: error
|
|
386
|
+
data: {"error":"Error message"}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Text Mode:**
|
|
390
|
+
```
|
|
391
|
+
{"error":"Error message"}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Route Documentation
|
|
395
|
+
|
|
396
|
+
Access generated documentation via `builder.docs`:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
interface StreamHttpRouteDoc {
|
|
400
|
+
name: string
|
|
401
|
+
path: string
|
|
402
|
+
methods: ('get' | 'post')[]
|
|
403
|
+
streamMode: 'sse' | 'text'
|
|
404
|
+
scope: string | string[]
|
|
405
|
+
version: number
|
|
406
|
+
jsonSchema: {
|
|
407
|
+
params?: Record<string, unknown> // From schema.params
|
|
408
|
+
yieldType?: Record<string, unknown> // SSE: envelope with data/event/id/retry; Text: from schema.yieldType
|
|
409
|
+
returnType?: Record<string, unknown> // From schema.returnType
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
In SSE mode, the documented `yieldType` is an SSE envelope schema with the user's `yieldType` nested under the `data` property:
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"type": "object",
|
|
419
|
+
"description": "SSE message envelope. The data field contains the procedure yield value.",
|
|
420
|
+
"required": ["data", "event", "id"],
|
|
421
|
+
"properties": {
|
|
422
|
+
"data": { /* user's yieldType schema */ },
|
|
423
|
+
"event": { "type": "string" },
|
|
424
|
+
"id": { "type": "string" },
|
|
425
|
+
"retry": { "type": "number" }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Procedure Filtering
|
|
431
|
+
|
|
432
|
+
Only streaming procedures (created with `CreateStream`) are registered. Regular procedures created with `Create` are ignored by this builder.
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
const RPC = Procedures<Context, RPCConfig>()
|
|
436
|
+
|
|
437
|
+
// This will NOT be registered by HonoStreamAppBuilder
|
|
438
|
+
RPC.Create('GetUser', config, async (ctx) => ({ user: 'data' }))
|
|
439
|
+
|
|
440
|
+
// This WILL be registered
|
|
441
|
+
RPC.CreateStream('WatchUser', config, async function* (ctx) {
|
|
442
|
+
yield { update: 'data' }
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Client Disconnect Handling
|
|
447
|
+
|
|
448
|
+
When a client disconnects, the stream's `onAbort` handler is triggered, which calls `generator.return()` to clean up. The `ctx.signal` in your handler will be aborted.
|
|
449
|
+
|
|
450
|
+
`HonoStreamAppBuilder` injects the HTTP request's `AbortSignal` (`c.req.raw.signal`) into the handler context. This is combined with the stream's internal `AbortController` via `AbortSignal.any()`, so `ctx.signal` aborts on either client disconnect or normal stream completion.
|
|
451
|
+
|
|
452
|
+
Use `signal.reason` to distinguish between the two:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
RPC.CreateStream('LongStream', config, async function* (ctx) {
|
|
456
|
+
try {
|
|
457
|
+
while (!ctx.signal.aborted) {
|
|
458
|
+
yield { tick: Date.now() }
|
|
459
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
if (ctx.signal.reason === 'stream-completed') {
|
|
463
|
+
// Generator finished normally
|
|
464
|
+
} else {
|
|
465
|
+
// Client disconnected — clean up resources
|
|
466
|
+
await releaseResources()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## TypeScript Types
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import {
|
|
476
|
+
HonoStreamAppBuilder,
|
|
477
|
+
HonoStreamAppBuilderConfig, // Generic: HonoStreamAppBuilderConfig<TErrorData = unknown>
|
|
478
|
+
StreamHttpRouteDoc,
|
|
479
|
+
StreamMode,
|
|
480
|
+
sse, // Helper to attach SSE metadata (event/id/retry) to yielded objects
|
|
481
|
+
SSEOptions, // Type for sse() options: { event?, id?, retry? }
|
|
482
|
+
MidStreamErrorResult, // Generic: MidStreamErrorResult<TErrorData = unknown>
|
|
483
|
+
} from 'ts-procedures/hono-stream'
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Migration Notes
|
|
487
|
+
|
|
488
|
+
### Migrating from v5
|
|
489
|
+
|
|
490
|
+
**Breaking: `MidStreamErrorResult` no longer has `event` and `id` fields.** Use `sse()` to attach SSE metadata to error data instead:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// Before (v5)
|
|
494
|
+
onMidStreamError: (proc, c, error) => ({
|
|
495
|
+
data: { type: 'error', message: error.message },
|
|
496
|
+
event: 'custom-error',
|
|
497
|
+
id: 'err-1',
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// After (v6)
|
|
501
|
+
onMidStreamError: (proc, c, error) => ({
|
|
502
|
+
data: sse(
|
|
503
|
+
{ type: 'error', message: error.message },
|
|
504
|
+
{ event: 'custom-error', id: 'err-1' }
|
|
505
|
+
),
|
|
506
|
+
})
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Breaking: `onStreamStart` / `onStreamEnd` now receive `streamMode` as a third parameter.**
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Before (v5)
|
|
513
|
+
onStreamStart: (procedure, c) => { ... }
|
|
514
|
+
onStreamEnd: (procedure, c) => { ... }
|
|
515
|
+
|
|
516
|
+
// After (v6)
|
|
517
|
+
onStreamStart: (procedure, c, streamMode) => { ... }
|
|
518
|
+
onStreamEnd: (procedure, c, streamMode) => { ... }
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Breaking: `StreamHttpRouteDoc.jsonSchema` fields narrowed from `object` to `Record<string, unknown>`.**
|
|
522
|
+
|
|
523
|
+
**New: Generic `TErrorData` parameter** on `HonoStreamAppBuilder` and `MidStreamErrorResult` for type-safe `onMidStreamError` callbacks.
|
|
524
|
+
|
|
525
|
+
**New: `onPreStreamError` error parameter** typed as `ProcedureValidationError | Error` for `instanceof` narrowing.
|