ts-procedures 5.4.0 → 5.5.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.
Files changed (50) hide show
  1. package/README.md +21 -0
  2. package/agent_config/claude-code/skills/guide/SKILL.md +1 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +37 -4
  4. package/agent_config/claude-code/skills/guide/api-reference.md +86 -0
  5. package/agent_config/claude-code/skills/guide/patterns.md +33 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +2 -0
  7. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +1 -1
  8. package/agent_config/copilot/copilot-instructions.md +4 -0
  9. package/agent_config/cursor/cursorrules +4 -0
  10. package/build/implementations/http/doc-registry.d.ts +12 -0
  11. package/build/implementations/http/doc-registry.js +114 -0
  12. package/build/implementations/http/doc-registry.js.map +1 -0
  13. package/build/implementations/http/doc-registry.test.d.ts +1 -0
  14. package/build/implementations/http/doc-registry.test.js +321 -0
  15. package/build/implementations/http/doc-registry.test.js.map +1 -0
  16. package/build/implementations/types.d.ts +31 -0
  17. package/package.json +5 -2
  18. package/src/errors.test.ts +0 -163
  19. package/src/errors.ts +0 -107
  20. package/src/exports.ts +0 -7
  21. package/src/implementations/http/README.md +0 -260
  22. package/src/implementations/http/express-rpc/README.md +0 -281
  23. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  24. package/src/implementations/http/express-rpc/index.ts +0 -265
  25. package/src/implementations/http/express-rpc/types.ts +0 -16
  26. package/src/implementations/http/hono-api/index.test.ts +0 -1328
  27. package/src/implementations/http/hono-api/index.ts +0 -461
  28. package/src/implementations/http/hono-api/types.ts +0 -16
  29. package/src/implementations/http/hono-rpc/README.md +0 -358
  30. package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
  31. package/src/implementations/http/hono-rpc/index.ts +0 -237
  32. package/src/implementations/http/hono-rpc/types.ts +0 -16
  33. package/src/implementations/http/hono-stream/README.md +0 -526
  34. package/src/implementations/http/hono-stream/index.test.ts +0 -1676
  35. package/src/implementations/http/hono-stream/index.ts +0 -435
  36. package/src/implementations/http/hono-stream/types.ts +0 -29
  37. package/src/implementations/types.ts +0 -127
  38. package/src/index.test.ts +0 -1194
  39. package/src/index.ts +0 -512
  40. package/src/schema/compute-schema.test.ts +0 -128
  41. package/src/schema/compute-schema.ts +0 -88
  42. package/src/schema/extract-json-schema.test.ts +0 -25
  43. package/src/schema/extract-json-schema.ts +0 -15
  44. package/src/schema/parser.test.ts +0 -182
  45. package/src/schema/parser.ts +0 -215
  46. package/src/schema/resolve-schema-lib.test.ts +0 -19
  47. package/src/schema/resolve-schema-lib.ts +0 -29
  48. package/src/schema/types.ts +0 -20
  49. package/src/stack-utils.test.ts +0 -94
  50. package/src/stack-utils.ts +0 -129
@@ -1,526 +0,0 @@
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. To avoid redundant validation, it passes `isPrevalidated: true` in the context when calling procedure handlers. This means:
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 is called with `isPrevalidated: true`
246
- 4. The procedure's internal validation is skipped, avoiding duplicate work
247
-
248
- This design allows for:
249
- - Clean HTTP error responses (400 status code, JSON body) instead of stream errors
250
- - No duplicate validation overhead
251
- - Consistent error handling at the framework level
252
-
253
- ## Error Handling
254
-
255
- Streaming has two distinct error phases with different semantics:
256
-
257
- | Phase | When | Response Type | Callback |
258
- |-------|------|---------------|----------|
259
- | **Pre-stream** | Validation, auth, context resolution | HTTP Response (400/500) | `onPreStreamError` |
260
- | **Mid-stream** | Generator throws during iteration | Value written to stream | `onMidStreamError` |
261
-
262
- ### Pre-Stream Errors (`onPreStreamError`)
263
-
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:
265
-
266
- ```typescript
267
- const builder = new HonoStreamAppBuilder({
268
- onPreStreamError: (procedure, c, error) => {
269
- if (error instanceof ProcedureValidationError) {
270
- return c.json({
271
- error: 'Invalid parameters',
272
- details: error.errors,
273
- procedure: procedure.name,
274
- }, 400)
275
- }
276
- return c.json({ error: error.message }, 500)
277
- },
278
- })
279
-
280
- // Without handler, returns default JSON response:
281
- // Status: 400 (validation) or 500 (other)
282
- // Body: { "error": "Validation error for ProcedureName - ..." }
283
- ```
284
-
285
- ### Mid-Stream Errors (`onMidStreamError`)
286
-
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()`:
290
-
291
- ```typescript
292
- const builder = new HonoStreamAppBuilder({
293
- onMidStreamError: (procedure, c, error) => {
294
- return {
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({
298
- type: 'error',
299
- code: 'STREAM_ERROR',
300
- message: error.message,
301
- retryable: false,
302
- }, { event: 'error', id: 'err-001' }),
303
- // Optional: whether to close stream after (default: true)
304
- closeStream: true,
305
- }
306
- },
307
- })
308
- ```
309
-
310
- **Return values:**
311
- - `{ data, closeStream? }` - Write `data` to stream. Use `sse(data, opts)` for custom SSE event/id/retry
312
- - `undefined` - Use default behavior: `{ error: message }` with `event: 'error'`
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
-
316
- ### Type-Safe Error Handling with Union Types
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
-
341
- For type-safe error handling, define your `yieldType` as a discriminated union:
342
-
343
- ```typescript
344
- StreamRPC.CreateStream('WatchEvents', {
345
- scope: 'events',
346
- version: 1,
347
- schema: {
348
- params: v.object({ roomId: v.string() }),
349
- // Union type: clients know exactly what to expect
350
- yieldType: v.union([
351
- v.object({
352
- type: v.literal('event'),
353
- eventType: v.string(),
354
- data: v.any()
355
- }),
356
- v.object({
357
- type: v.literal('error'),
358
- code: v.string(),
359
- message: v.string(),
360
- retryable: v.boolean()
361
- })
362
- ])
363
- }
364
- }, async function* (ctx, params) {
365
- // Yield domain data directly — matches yieldType schema
366
- yield { type: 'event', eventType: 'user_joined', data: { userId: ctx.userId } }
367
- // On error, the procedure throws and onMidStreamError handles it
368
- })
369
- ```
370
-
371
- **Output (SSE mode):**
372
- ```
373
- event: WatchEvents
374
- data: {"type":"event","eventType":"user_joined","data":{"userId":"123"}}
375
-
376
- event: WatchEvents
377
- data: {"type":"error","code":"STREAM_ERROR","message":"Connection lost","retryable":false}
378
- ```
379
-
380
- ### Default Error Output
381
-
382
- Without custom callbacks, errors are written as:
383
-
384
- **SSE Mode:**
385
- ```
386
- event: error
387
- data: {"error":"Error message"}
388
- ```
389
-
390
- **Text Mode:**
391
- ```
392
- {"error":"Error message"}
393
- ```
394
-
395
- ## Route Documentation
396
-
397
- Access generated documentation via `builder.docs`:
398
-
399
- ```typescript
400
- interface StreamHttpRouteDoc {
401
- name: string
402
- path: string
403
- methods: ('get' | 'post')[]
404
- streamMode: 'sse' | 'text'
405
- scope: string | string[]
406
- version: number
407
- jsonSchema: {
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" }
427
- }
428
- }
429
- ```
430
-
431
- ## Procedure Filtering
432
-
433
- Only streaming procedures (created with `CreateStream`) are registered. Regular procedures created with `Create` are ignored by this builder.
434
-
435
- ```typescript
436
- const RPC = Procedures<Context, RPCConfig>()
437
-
438
- // This will NOT be registered by HonoStreamAppBuilder
439
- RPC.Create('GetUser', config, async (ctx) => ({ user: 'data' }))
440
-
441
- // This WILL be registered
442
- RPC.CreateStream('WatchUser', config, async function* (ctx) {
443
- yield { update: 'data' }
444
- })
445
- ```
446
-
447
- ## Client Disconnect Handling
448
-
449
- 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.
450
-
451
- `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.
452
-
453
- Use `signal.reason` to distinguish between the two:
454
-
455
- ```typescript
456
- RPC.CreateStream('LongStream', config, async function* (ctx) {
457
- try {
458
- while (!ctx.signal.aborted) {
459
- yield { tick: Date.now() }
460
- await new Promise((r) => setTimeout(r, 1000))
461
- }
462
- } finally {
463
- if (ctx.signal.reason === 'stream-completed') {
464
- // Generator finished normally
465
- } else {
466
- // Client disconnected — clean up resources
467
- await releaseResources()
468
- }
469
- }
470
- })
471
- ```
472
-
473
- ## TypeScript Types
474
-
475
- ```typescript
476
- import {
477
- HonoStreamAppBuilder,
478
- HonoStreamAppBuilderConfig, // Generic: HonoStreamAppBuilderConfig<TErrorData = unknown>
479
- StreamHttpRouteDoc,
480
- StreamMode,
481
- sse, // Helper to attach SSE metadata (event/id/retry) to yielded objects
482
- SSEOptions, // Type for sse() options: { event?, id?, retry? }
483
- MidStreamErrorResult, // Generic: MidStreamErrorResult<TErrorData = unknown>
484
- } from 'ts-procedures/hono-stream'
485
- ```
486
-
487
- ## Migration Notes
488
-
489
- ### Migrating from v5
490
-
491
- **Breaking: `MidStreamErrorResult` no longer has `event` and `id` fields.** Use `sse()` to attach SSE metadata to error data instead:
492
-
493
- ```typescript
494
- // Before (v5)
495
- onMidStreamError: (proc, c, error) => ({
496
- data: { type: 'error', message: error.message },
497
- event: 'custom-error',
498
- id: 'err-1',
499
- })
500
-
501
- // After (v6)
502
- onMidStreamError: (proc, c, error) => ({
503
- data: sse(
504
- { type: 'error', message: error.message },
505
- { event: 'custom-error', id: 'err-1' }
506
- ),
507
- })
508
- ```
509
-
510
- **Breaking: `onStreamStart` / `onStreamEnd` now receive `streamMode` as a third parameter.**
511
-
512
- ```typescript
513
- // Before (v5)
514
- onStreamStart: (procedure, c) => { ... }
515
- onStreamEnd: (procedure, c) => { ... }
516
-
517
- // After (v6)
518
- onStreamStart: (procedure, c, streamMode) => { ... }
519
- onStreamEnd: (procedure, c, streamMode) => { ... }
520
- ```
521
-
522
- **Breaking: `StreamHttpRouteDoc.jsonSchema` fields narrowed from `object` to `Record<string, unknown>`.**
523
-
524
- **New: Generic `TErrorData` parameter** on `HonoStreamAppBuilder` and `MidStreamErrorResult` for type-safe `onMidStreamError` callbacks.
525
-
526
- **New: `onPreStreamError` error parameter** typed as `ProcedureValidationError | Error` for `instanceof` narrowing.