ts-procedures 4.0.1 → 5.1.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/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +17 -18
- package/build/implementations/http/hono-stream/index.js +38 -37
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +306 -61
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/hono-stream/types.d.ts +3 -3
- package/build/implementations/types.d.ts +5 -5
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/index.ts +1 -1
- package/src/implementations/http/hono-rpc/index.ts +1 -1
- package/src/implementations/http/hono-stream/README.md +151 -67
- package/src/implementations/http/hono-stream/index.test.ts +374 -66
- package/src/implementations/http/hono-stream/index.ts +62 -58
- package/src/implementations/http/hono-stream/types.ts +3 -3
- package/src/implementations/types.ts +5 -5
|
@@ -18,7 +18,7 @@ npm install hono
|
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
import { Procedures } from 'ts-procedures'
|
|
21
|
-
import { HonoStreamAppBuilder } from 'ts-procedures/hono-stream'
|
|
21
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
22
22
|
|
|
23
23
|
// Define your context and config types
|
|
24
24
|
type StreamContext = { userId: string }
|
|
@@ -30,7 +30,7 @@ interface RPCConfig {
|
|
|
30
30
|
// Create a procedures factory
|
|
31
31
|
const StreamRPC = Procedures<StreamContext, RPCConfig>()
|
|
32
32
|
|
|
33
|
-
// Create a streaming procedure
|
|
33
|
+
// Create a streaming procedure — yield domain data directly
|
|
34
34
|
StreamRPC.CreateStream(
|
|
35
35
|
'WatchNotifications',
|
|
36
36
|
{
|
|
@@ -42,8 +42,8 @@ StreamRPC.CreateStream(
|
|
|
42
42
|
},
|
|
43
43
|
async function* (ctx) {
|
|
44
44
|
for (let i = 1; i <= 10; i++) {
|
|
45
|
-
//
|
|
46
|
-
yield {
|
|
45
|
+
// Yield domain data directly — matches yieldType schema
|
|
46
|
+
yield { id: i, message: `Notification ${i}` }
|
|
47
47
|
await new Promise((r) => setTimeout(r, 1000))
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -73,41 +73,52 @@ POST /{pathPrefix}/{scope...}/{procedureName}/{version} (JSON body)
|
|
|
73
73
|
- **GET**: For EventSource/SSE clients, params passed via query string
|
|
74
74
|
- **POST**: For clients needing complex JSON body params
|
|
75
75
|
|
|
76
|
-
## SSE
|
|
76
|
+
## Yielding in SSE Mode
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Procedures yield **domain data directly** — the same shape as your `yieldType` schema. `HonoStreamAppBuilder` handles the SSE envelope (`event:`, `id:`, `data:`) automatically.
|
|
79
79
|
|
|
80
80
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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'
|
|
86
87
|
}
|
|
87
88
|
```
|
|
88
89
|
|
|
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:
|
|
90
95
|
|
|
91
96
|
```typescript
|
|
92
|
-
|
|
93
|
-
// Minimal: only data required (event defaults to procedure name, id auto-increments)
|
|
94
|
-
yield { data: { count: 1 } }
|
|
97
|
+
import { sse } from 'ts-procedures/hono-stream'
|
|
95
98
|
|
|
99
|
+
async function* (ctx) {
|
|
96
100
|
// Custom event name
|
|
97
|
-
yield {
|
|
101
|
+
yield sse({ type: 'heartbeat' }, { event: 'ping' })
|
|
98
102
|
|
|
99
103
|
// Custom id
|
|
100
|
-
yield {
|
|
104
|
+
yield sse({ msg: 'hello' }, { id: 'msg-001' })
|
|
101
105
|
|
|
102
|
-
//
|
|
103
|
-
yield {
|
|
106
|
+
// All SSE options
|
|
107
|
+
yield sse({ done: true }, { event: 'complete', id: 'final', retry: 5000 })
|
|
104
108
|
|
|
105
|
-
//
|
|
106
|
-
yield {
|
|
109
|
+
// sse() with no options — same as plain yield (uses defaults)
|
|
110
|
+
yield sse({ count: 1 })
|
|
107
111
|
}
|
|
108
112
|
```
|
|
109
113
|
|
|
110
|
-
|
|
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.
|
|
111
122
|
|
|
112
123
|
## Stream Modes
|
|
113
124
|
|
|
@@ -125,7 +136,7 @@ event: WatchNotifications
|
|
|
125
136
|
data: {"id":1,"message":"Notification 1"}
|
|
126
137
|
id: 0
|
|
127
138
|
|
|
128
|
-
event:
|
|
139
|
+
event: WatchNotifications
|
|
129
140
|
data: {"id":2,"message":"Notification 2"}
|
|
130
141
|
id: 1
|
|
131
142
|
```
|
|
@@ -138,7 +149,7 @@ eventSource.addEventListener('WatchNotifications', (event) => {
|
|
|
138
149
|
const data = JSON.parse(event.data)
|
|
139
150
|
console.log(data)
|
|
140
151
|
})
|
|
141
|
-
// Listen for custom events
|
|
152
|
+
// Listen for custom events (when using sse() helper)
|
|
142
153
|
eventSource.addEventListener('notification', (event) => {
|
|
143
154
|
const data = JSON.parse(event.data)
|
|
144
155
|
console.log(data)
|
|
@@ -180,18 +191,18 @@ while (true) {
|
|
|
180
191
|
### Builder Config
|
|
181
192
|
|
|
182
193
|
```typescript
|
|
183
|
-
interface HonoStreamAppBuilderConfig {
|
|
194
|
+
interface HonoStreamAppBuilderConfig<TErrorData = unknown> {
|
|
184
195
|
app?: Hono // Use existing Hono instance
|
|
185
196
|
pathPrefix?: string // Prefix for all routes
|
|
186
197
|
defaultStreamMode?: 'sse' | 'text' // Default: 'sse'
|
|
187
198
|
onRequestStart?: (c: Context) => void
|
|
188
199
|
onRequestEnd?: (c: Context) => void
|
|
189
|
-
onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context) => void
|
|
190
|
-
onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context) => void
|
|
200
|
+
onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
201
|
+
onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
|
|
191
202
|
|
|
192
203
|
// Error handling callbacks (see Error Handling section)
|
|
193
|
-
onPreStreamError?: (procedure, c, error) => Response | Promise<Response>
|
|
194
|
-
onMidStreamError?: (procedure, c, error) =>
|
|
204
|
+
onPreStreamError?: (procedure, c, error: ProcedureValidationError | Error) => Response | Promise<Response>
|
|
205
|
+
onMidStreamError?: (procedure, c, error) => MidStreamErrorResult<TErrorData> | undefined
|
|
195
206
|
}
|
|
196
207
|
```
|
|
197
208
|
|
|
@@ -212,15 +223,17 @@ builder.register(factory, context, {
|
|
|
212
223
|
Hooks execute in the following order:
|
|
213
224
|
|
|
214
225
|
```
|
|
215
|
-
onRequestStart → [validation] → onStreamStart → [yields...] → onStreamEnd → onRequestEnd
|
|
216
|
-
↓
|
|
217
|
-
(validation error)
|
|
218
|
-
↓
|
|
219
|
-
HTTP 400 response
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
233
|
```
|
|
223
234
|
|
|
235
|
+
`onStreamStart` and `onStreamEnd` receive the resolved `streamMode` (`'sse'` or `'text'`) as their third argument.
|
|
236
|
+
|
|
224
237
|
## Validation
|
|
225
238
|
|
|
226
239
|
### Pre-validation
|
|
@@ -248,7 +261,7 @@ Streaming has two distinct error phases with different semantics:
|
|
|
248
261
|
|
|
249
262
|
### Pre-Stream Errors (`onPreStreamError`)
|
|
250
263
|
|
|
251
|
-
Errors that occur **before** the stream starts (validation failures, context resolution errors). The return value **IS** used as the HTTP response:
|
|
264
|
+
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:
|
|
252
265
|
|
|
253
266
|
```typescript
|
|
254
267
|
const builder = new HonoStreamAppBuilder({
|
|
@@ -271,23 +284,22 @@ const builder = new HonoStreamAppBuilder({
|
|
|
271
284
|
|
|
272
285
|
### Mid-Stream Errors (`onMidStreamError`)
|
|
273
286
|
|
|
274
|
-
Errors that occur **during** streaming (generator throws). Since the stream is already open, HTTP status cannot change.
|
|
287
|
+
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.
|
|
288
|
+
|
|
289
|
+
To control SSE metadata (event name, id, retry) on the error yield, wrap the data object with `sse()`:
|
|
275
290
|
|
|
276
291
|
```typescript
|
|
277
292
|
const builder = new HonoStreamAppBuilder({
|
|
278
293
|
onMidStreamError: (procedure, c, error) => {
|
|
279
294
|
return {
|
|
280
|
-
// Data
|
|
281
|
-
|
|
295
|
+
// Data written as the SSE data: field (should match your yieldType schema)
|
|
296
|
+
// Use sse() to attach custom event/id/retry metadata
|
|
297
|
+
data: sse({
|
|
282
298
|
type: 'error',
|
|
283
299
|
code: 'STREAM_ERROR',
|
|
284
300
|
message: error.message,
|
|
285
301
|
retryable: false,
|
|
286
|
-
},
|
|
287
|
-
// Optional SSE event name (defaults to procedure name if data provided, 'error' otherwise)
|
|
288
|
-
event: 'error',
|
|
289
|
-
// Optional SSE event id (auto-incremented if not provided)
|
|
290
|
-
id: 'err-001',
|
|
302
|
+
}, { event: 'error', id: 'err-001' }),
|
|
291
303
|
// Optional: whether to close stream after (default: true)
|
|
292
304
|
closeStream: true,
|
|
293
305
|
}
|
|
@@ -296,11 +308,36 @@ const builder = new HonoStreamAppBuilder({
|
|
|
296
308
|
```
|
|
297
309
|
|
|
298
310
|
**Return values:**
|
|
299
|
-
- `{ data,
|
|
311
|
+
- `{ data, closeStream? }` - Write `data` to stream. Use `sse(data, opts)` for custom SSE event/id/retry
|
|
300
312
|
- `undefined` - Use default behavior: `{ error: message }` with `event: 'error'`
|
|
301
313
|
|
|
314
|
+
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.
|
|
315
|
+
|
|
302
316
|
### Type-Safe Error Handling with Union Types
|
|
303
317
|
|
|
318
|
+
Use the generic `TErrorData` parameter on `HonoStreamAppBuilder` to constrain the `onMidStreamError` return type:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
type ErrorPayload = {
|
|
322
|
+
type: 'error'
|
|
323
|
+
code: string
|
|
324
|
+
message: string
|
|
325
|
+
retryable: boolean
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// TErrorData constrains what onMidStreamError can return
|
|
329
|
+
const builder = new HonoStreamAppBuilder<ErrorPayload>({
|
|
330
|
+
onMidStreamError: (proc, c, error) => ({
|
|
331
|
+
data: {
|
|
332
|
+
type: 'error',
|
|
333
|
+
code: 'STREAM_ERROR',
|
|
334
|
+
message: error.message,
|
|
335
|
+
retryable: false,
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
```
|
|
340
|
+
|
|
304
341
|
For type-safe error handling, define your `yieldType` as a discriminated union:
|
|
305
342
|
|
|
306
343
|
```typescript
|
|
@@ -325,21 +362,10 @@ StreamRPC.CreateStream('WatchEvents', {
|
|
|
325
362
|
])
|
|
326
363
|
}
|
|
327
364
|
}, async function* (ctx, params) {
|
|
328
|
-
|
|
365
|
+
// Yield domain data directly — matches yieldType schema
|
|
366
|
+
yield { type: 'event', eventType: 'user_joined', data: { userId: ctx.userId } }
|
|
329
367
|
// On error, the procedure throws and onMidStreamError handles it
|
|
330
368
|
})
|
|
331
|
-
|
|
332
|
-
// Configure error callback to return union-compatible error value
|
|
333
|
-
const builder = new HonoStreamAppBuilder({
|
|
334
|
-
onMidStreamError: (proc, c, error) => ({
|
|
335
|
-
data: {
|
|
336
|
-
type: 'error',
|
|
337
|
-
code: 'STREAM_ERROR',
|
|
338
|
-
message: error.message,
|
|
339
|
-
retryable: false,
|
|
340
|
-
}
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
369
|
```
|
|
344
370
|
|
|
345
371
|
**Output (SSE mode):**
|
|
@@ -379,9 +405,25 @@ interface StreamHttpRouteDoc {
|
|
|
379
405
|
scope: string | string[]
|
|
380
406
|
version: number
|
|
381
407
|
jsonSchema: {
|
|
382
|
-
params?:
|
|
383
|
-
yieldType?:
|
|
384
|
-
returnType?:
|
|
408
|
+
params?: Record<string, unknown> // From schema.params
|
|
409
|
+
yieldType?: Record<string, unknown> // SSE: envelope with data/event/id/retry; Text: from schema.yieldType
|
|
410
|
+
returnType?: Record<string, unknown> // From schema.returnType
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
In SSE mode, the documented `yieldType` is an SSE envelope schema with the user's `yieldType` nested under the `data` property:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"type": "object",
|
|
420
|
+
"description": "SSE message envelope. The data field contains the procedure yield value.",
|
|
421
|
+
"required": ["data", "event", "id"],
|
|
422
|
+
"properties": {
|
|
423
|
+
"data": { /* user's yieldType schema */ },
|
|
424
|
+
"event": { "type": "string" },
|
|
425
|
+
"id": { "type": "string" },
|
|
426
|
+
"retry": { "type": "number" }
|
|
385
427
|
}
|
|
386
428
|
}
|
|
387
429
|
```
|
|
@@ -398,7 +440,7 @@ RPC.Create('GetUser', config, async (ctx) => ({ user: 'data' }))
|
|
|
398
440
|
|
|
399
441
|
// This WILL be registered
|
|
400
442
|
RPC.CreateStream('WatchUser', config, async function* (ctx) {
|
|
401
|
-
yield {
|
|
443
|
+
yield { update: 'data' }
|
|
402
444
|
})
|
|
403
445
|
```
|
|
404
446
|
|
|
@@ -409,7 +451,7 @@ When a client disconnects, the stream's `onAbort` handler is triggered, which ca
|
|
|
409
451
|
```typescript
|
|
410
452
|
RPC.CreateStream('LongStream', config, async function* (ctx) {
|
|
411
453
|
while (!ctx.signal.aborted) {
|
|
412
|
-
yield {
|
|
454
|
+
yield { tick: Date.now() }
|
|
413
455
|
await new Promise((r) => setTimeout(r, 1000))
|
|
414
456
|
}
|
|
415
457
|
})
|
|
@@ -420,10 +462,52 @@ RPC.CreateStream('LongStream', config, async function* (ctx) {
|
|
|
420
462
|
```typescript
|
|
421
463
|
import {
|
|
422
464
|
HonoStreamAppBuilder,
|
|
423
|
-
HonoStreamAppBuilderConfig,
|
|
465
|
+
HonoStreamAppBuilderConfig, // Generic: HonoStreamAppBuilderConfig<TErrorData = unknown>
|
|
424
466
|
StreamHttpRouteDoc,
|
|
425
467
|
StreamMode,
|
|
426
|
-
|
|
427
|
-
|
|
468
|
+
sse, // Helper to attach SSE metadata (event/id/retry) to yielded objects
|
|
469
|
+
SSEOptions, // Type for sse() options: { event?, id?, retry? }
|
|
470
|
+
MidStreamErrorResult, // Generic: MidStreamErrorResult<TErrorData = unknown>
|
|
428
471
|
} from 'ts-procedures/hono-stream'
|
|
429
472
|
```
|
|
473
|
+
|
|
474
|
+
## Migration Notes
|
|
475
|
+
|
|
476
|
+
### Migrating from v5
|
|
477
|
+
|
|
478
|
+
**Breaking: `MidStreamErrorResult` no longer has `event` and `id` fields.** Use `sse()` to attach SSE metadata to error data instead:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Before (v5)
|
|
482
|
+
onMidStreamError: (proc, c, error) => ({
|
|
483
|
+
data: { type: 'error', message: error.message },
|
|
484
|
+
event: 'custom-error',
|
|
485
|
+
id: 'err-1',
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
// After (v6)
|
|
489
|
+
onMidStreamError: (proc, c, error) => ({
|
|
490
|
+
data: sse(
|
|
491
|
+
{ type: 'error', message: error.message },
|
|
492
|
+
{ event: 'custom-error', id: 'err-1' }
|
|
493
|
+
),
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Breaking: `onStreamStart` / `onStreamEnd` now receive `streamMode` as a third parameter.**
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// Before (v5)
|
|
501
|
+
onStreamStart: (procedure, c) => { ... }
|
|
502
|
+
onStreamEnd: (procedure, c) => { ... }
|
|
503
|
+
|
|
504
|
+
// After (v6)
|
|
505
|
+
onStreamStart: (procedure, c, streamMode) => { ... }
|
|
506
|
+
onStreamEnd: (procedure, c, streamMode) => { ... }
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Breaking: `StreamHttpRouteDoc.jsonSchema` fields narrowed from `object` to `Record<string, unknown>`.**
|
|
510
|
+
|
|
511
|
+
**New: Generic `TErrorData` parameter** on `HonoStreamAppBuilder` and `MidStreamErrorResult` for type-safe `onMidStreamError` callbacks.
|
|
512
|
+
|
|
513
|
+
**New: `onPreStreamError` error parameter** typed as `ProcedureValidationError | Error` for `instanceof` narrowing.
|