ts-procedures 3.2.0 → 3.3.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 (39) hide show
  1. package/README.md +222 -2
  2. package/build/errors.d.ts +5 -0
  3. package/build/errors.js +14 -0
  4. package/build/errors.js.map +1 -1
  5. package/build/implementations/http/hono-stream/index.d.ts +92 -0
  6. package/build/implementations/http/hono-stream/index.js +229 -0
  7. package/build/implementations/http/hono-stream/index.js.map +1 -0
  8. package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
  9. package/build/implementations/http/hono-stream/index.test.js +681 -0
  10. package/build/implementations/http/hono-stream/index.test.js.map +1 -0
  11. package/build/implementations/http/hono-stream/types.d.ts +24 -0
  12. package/build/implementations/http/hono-stream/types.js +2 -0
  13. package/build/implementations/http/hono-stream/types.js.map +1 -0
  14. package/build/implementations/types.d.ts +15 -1
  15. package/build/index.d.ts +62 -3
  16. package/build/index.js +96 -1
  17. package/build/index.js.map +1 -1
  18. package/build/index.test.js +283 -2
  19. package/build/index.test.js.map +1 -1
  20. package/build/schema/compute-schema.d.ts +6 -1
  21. package/build/schema/compute-schema.js +4 -1
  22. package/build/schema/compute-schema.js.map +1 -1
  23. package/build/schema/parser.d.ts +6 -0
  24. package/build/schema/parser.js +42 -0
  25. package/build/schema/parser.js.map +1 -1
  26. package/build/schema/types.d.ts +1 -0
  27. package/package.json +1 -1
  28. package/src/errors.ts +16 -0
  29. package/src/implementations/http/README.md +87 -55
  30. package/src/implementations/http/hono-stream/README.md +261 -0
  31. package/src/implementations/http/hono-stream/index.test.ts +1009 -0
  32. package/src/implementations/http/hono-stream/index.ts +327 -0
  33. package/src/implementations/http/hono-stream/types.ts +29 -0
  34. package/src/implementations/types.ts +17 -1
  35. package/src/index.test.ts +402 -44
  36. package/src/index.ts +189 -3
  37. package/src/schema/compute-schema.ts +10 -3
  38. package/src/schema/parser.ts +55 -4
  39. package/src/schema/types.ts +4 -0
@@ -1,19 +1,38 @@
1
- # HTTP-RPC Implementations
1
+ # HTTP Implementations
2
2
 
3
- HTTP-RPC builders for `ts-procedures` that create type-safe, versioned RPC endpoints with automatic path generation, schema-based validation, and route documentation.
3
+ HTTP implementation builders for `ts-procedures` that create type-safe, versioned endpoints with automatic path generation, schema-based validation, and route documentation.
4
4
 
5
5
  ## Available Implementations
6
6
 
7
+ ### RPC (Request/Response)
8
+
9
+ For procedures created with `Create()` - standard request/response pattern using POST.
10
+
11
+ | Framework | Package | Description |
12
+ |-----------|---------|-------------|
13
+ | [Express RPC](./express-rpc/README.md) | `express-rpc` | Express.js integration |
14
+ | [Hono RPC](./hono-rpc/README.md) | `hono-rpc` | Hono integration (Bun, Deno, Cloudflare Workers, Node.js) |
15
+
16
+ ### Streaming
17
+
18
+ For procedures created with `CreateStream()` - server-sent events and streaming responses.
19
+
7
20
  | Framework | Package | Description |
8
21
  |-----------|---------|-------------|
9
- | [Express](./express-rpc/README.md) | `express-rpc` | Express.js integration |
10
- | [Hono](./hono-rpc/README.md) | `hono-rpc` | Hono integration (Bun, Deno, Cloudflare Workers, Node.js) |
22
+ | [Hono Stream](./hono-stream/README.md) | `hono-stream` | SSE and text streaming for async generators |
23
+
24
+ ## Procedure Types
25
+
26
+ | Type | Created With | Handler Return | HTTP Methods | Use Case |
27
+ |------|--------------|----------------|--------------|----------|
28
+ | RPC | `Create()` | `Promise<T>` | POST | Standard request/response |
29
+ | Stream | `CreateStream()` | `AsyncGenerator<T>` | GET, POST | Real-time updates, SSE |
11
30
 
12
31
  ## Core Concepts
13
32
 
14
- ### RPCConfig Interface
33
+ ### Config Interface
15
34
 
16
- All HTTP-RPC implementations use a shared configuration interface:
35
+ All HTTP implementations use a shared configuration interface:
17
36
 
18
37
  ```typescript
19
38
  interface RPCConfig {
@@ -24,13 +43,13 @@ interface RPCConfig {
24
43
 
25
44
  ### Path Generation
26
45
 
27
- Routes are generated using kebab-case conversion with the formula:
46
+ Routes are generated using kebab-case conversion:
28
47
 
29
48
  ```
30
49
  /{pathPrefix}/{scope...}/{procedureName}/{version}
31
50
  ```
32
51
 
33
- **Conversion Examples:**
52
+ **Examples:**
34
53
 
35
54
  | Scope | Procedure Name | Version | Generated Path |
36
55
  |-------|----------------|---------|----------------|
@@ -46,85 +65,98 @@ Routes are generated using kebab-case conversion with the formula:
46
65
  | `'users'` | `'Create'` | `1` | `/api/v1/users/create/1` |
47
66
  | `['users', 'admin']` | `'Delete'` | `2` | `/api/v1/users/admin/delete/2` |
48
67
 
49
- ### Context Resolution Patterns
68
+ ### Context Resolution
50
69
 
51
70
  The `factoryContext` parameter supports three patterns:
52
71
 
53
72
  ```typescript
54
73
  // 1. Static object
55
- builder.register(RPC, { userId: 'static-123' })
74
+ builder.register(Factory, { userId: 'static-123' })
56
75
 
57
76
  // 2. Sync function
58
- builder.register(RPC, (req) => ({
59
- userId: req.headers['x-user-id']
77
+ builder.register(Factory, (c) => ({
78
+ userId: c.req.header('x-user-id')
60
79
  }))
61
80
 
62
81
  // 3. Async function
63
- builder.register(RPC, async (req) => {
64
- const user = await validateToken(req.headers.authorization)
82
+ builder.register(Factory, async (c) => {
83
+ const user = await validateToken(c.req.header('authorization'))
65
84
  return { userId: user.id }
66
85
  })
67
86
  ```
68
87
 
69
88
  ### Lifecycle Hooks
70
89
 
71
- Hooks execute in the following order:
90
+ **RPC Implementations:**
72
91
 
73
92
  ```
74
93
  onRequestStart → handler → onSuccess → onRequestEnd
75
94
 
76
95
  (on error)
77
96
 
78
- error handler
97
+ onError handler
79
98
 
80
- onRequestEnd
99
+ onRequestEnd
100
+ ```
101
+
102
+ **Stream Implementations:**
103
+
104
+ ```
105
+ onRequestStart → onStreamStart → [yields...] → onStreamEnd → onRequestEnd
106
+
107
+ (on error)
108
+
109
+ error in stream
110
+
111
+ onStreamEnd
81
112
  ```
82
113
 
83
- | Hook | Trigger | Use Case |
84
- |------|---------|----------|
85
- | `onRequestStart` | Before route handler | Logging, request tracking |
86
- | `onSuccess` | After successful handler execution | Metrics, audit logging |
87
- | `onRequestEnd` | After response sent | Cleanup, timing metrics |
88
- | `error` | On handler error | Custom error responses |
114
+ | Hook | Available In | Trigger |
115
+ |------|--------------|---------|
116
+ | `onRequestStart` | Both | Before route handler |
117
+ | `onRequestEnd` | Both | After response sent |
118
+ | `onSuccess` | RPC | After successful handler |
119
+ | `onError` | RPC | On handler error |
120
+ | `onStreamStart` | Stream | Before first yield |
121
+ | `onStreamEnd` | Stream | After stream completes |
122
+ | `onStreamError` | Stream | On stream error |
89
123
 
90
124
  ### Route Documentation
91
125
 
92
- Each registered procedure generates an `RPCHttpRouteDoc`:
126
+ Each registered procedure generates documentation accessible via `builder.docs`.
127
+
128
+ **RPC Documentation (`RPCHttpRouteDoc`):**
93
129
 
94
130
  ```typescript
95
131
  interface RPCHttpRouteDoc {
96
- path: string // Generated route path
97
- method: 'post' // Always POST for RPC
132
+ name: string
133
+ path: string
134
+ method: 'post'
135
+ scope: string | string[]
136
+ version: number
98
137
  jsonSchema: {
99
- body?: object // JSON Schema from schema.params
100
- response?: object // JSON Schema from schema.returnType
138
+ body?: object // From schema.params
139
+ response?: object // From schema.returnType
101
140
  }
102
141
  }
103
142
  ```
104
143
 
105
- Access documentation via `builder.docs` after calling `build()`:
144
+ **Stream Documentation (`StreamHttpRouteDoc`):**
106
145
 
107
146
  ```typescript
108
- const builder = new ExpressRPCAppBuilder()
109
- builder.register(RPC, () => ({}))
110
- builder.build()
111
-
112
- // Generate OpenAPI documentation
113
- const openApiPaths = builder.docs.reduce((acc, doc) => {
114
- acc[doc.path] = {
115
- post: {
116
- requestBody: doc.jsonSchema.body ? {
117
- content: { 'application/json': { schema: doc.jsonSchema.body } }
118
- } : undefined,
119
- responses: {
120
- 200: doc.jsonSchema.response ? {
121
- content: { 'application/json': { schema: doc.jsonSchema.response } }
122
- } : undefined
123
- }
124
- }
147
+ interface StreamHttpRouteDoc {
148
+ name: string
149
+ path: string
150
+ methods: ('get' | 'post')[]
151
+ streamMode: 'sse' | 'text'
152
+ scope: string | string[]
153
+ version: number
154
+ jsonSchema: {
155
+ params?: object // From schema.params
156
+ yieldType?: object // From schema.yieldType
157
+ returnType?: object // From schema.returnType
125
158
  }
126
- return acc
127
- }, {})
159
+ }
128
160
  ```
129
161
 
130
162
  ### Builder Pattern
@@ -132,9 +164,9 @@ const openApiPaths = builder.docs.reduce((acc, doc) => {
132
164
  All implementations follow the same builder pattern:
133
165
 
134
166
  ```typescript
135
- const builder = new RPCAppBuilder(config)
136
- .register(PublicRPC, publicContextResolver)
137
- .register(ProtectedRPC, protectedContextResolver)
167
+ const builder = new AppBuilder(config)
168
+ .register(PublicFactory, publicContextResolver)
169
+ .register(ProtectedFactory, protectedContextResolver)
138
170
 
139
171
  const app = builder.build()
140
172
  const docs = builder.docs
@@ -144,16 +176,15 @@ const docs = builder.docs
144
176
 
145
177
  | Method | Returns | Description |
146
178
  |--------|---------|-------------|
147
- | `register(factory, context)` | `this` | Register a procedure factory with context resolver |
179
+ | `register(factory, context, options?)` | `this` | Register a procedure factory |
148
180
  | `build()` | Framework app | Create routes and return the application |
149
- | `makeRPCHttpRoutePath(config)` | `string` | Generate path for an RPCConfig |
150
181
 
151
182
  **Properties:**
152
183
 
153
184
  | Property | Type | Description |
154
185
  |----------|------|-------------|
155
186
  | `app` | Framework app | The underlying framework application |
156
- | `docs` | `RPCHttpRouteDoc[]` | Route documentation (populated after `build()`) |
187
+ | `docs` | Route doc array | Route documentation (populated after `build()`) |
157
188
 
158
189
  ## Framework Comparison
159
190
 
@@ -164,9 +195,10 @@ const docs = builder.docs
164
195
  | Body access | `req.body` | `await c.req.json()` |
165
196
  | Header access | `req.headers['x-id']` | `c.req.header('x-id')` |
166
197
  | JSON middleware | Auto-added (or manual) | Built-in |
198
+ | Streaming support | Not yet | `hono-stream` |
167
199
 
168
200
  ## TypeScript Types
169
201
 
170
202
  ```typescript
171
- import { RPCConfig, RPCHttpRouteDoc } from 'ts-procedures/http-rpc'
203
+ import { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/implementations/types'
172
204
  ```
@@ -0,0 +1,261 @@
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 } from 'ts-procedures/implementations/http/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
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 { id: i, message: `Notification ${i}` }
46
+ await new Promise((r) => setTimeout(r, 1000))
47
+ }
48
+ }
49
+ )
50
+
51
+ // Build the Hono app
52
+ const builder = new HonoStreamAppBuilder()
53
+ .register(StreamRPC, (c) => ({
54
+ userId: c.req.header('x-user-id') || 'anonymous',
55
+ }))
56
+
57
+ const app = builder.build()
58
+
59
+ // Access documentation
60
+ const docs = builder.docs
61
+ ```
62
+
63
+ ## HTTP Methods
64
+
65
+ Both GET and POST methods are supported for each streaming endpoint:
66
+
67
+ ```
68
+ GET /{pathPrefix}/{scope...}/{procedureName}/{version}?param1=value1
69
+ POST /{pathPrefix}/{scope...}/{procedureName}/{version} (JSON body)
70
+ ```
71
+
72
+ - **GET**: For EventSource/SSE clients, params passed via query string
73
+ - **POST**: For clients needing complex JSON body params
74
+
75
+ ## Stream Modes
76
+
77
+ ### SSE Mode (default)
78
+
79
+ Returns `text/event-stream` content type with SSE-formatted messages:
80
+
81
+ ```typescript
82
+ const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
83
+ ```
84
+
85
+ Response format:
86
+ ```
87
+ event: ProcedureName
88
+ data: {"key":"value"}
89
+ id: 0
90
+
91
+ event: ProcedureName
92
+ data: {"key":"value2"}
93
+ id: 1
94
+ ```
95
+
96
+ Client usage:
97
+ ```typescript
98
+ const eventSource = new EventSource('/user/notifications/watch-notifications/1')
99
+ eventSource.addEventListener('WatchNotifications', (event) => {
100
+ const data = JSON.parse(event.data)
101
+ console.log(data)
102
+ })
103
+ ```
104
+
105
+ ### Text Mode
106
+
107
+ Returns `text/plain` content type with newline-delimited JSON:
108
+
109
+ ```typescript
110
+ const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
111
+ ```
112
+
113
+ Response format:
114
+ ```
115
+ {"key":"value"}
116
+ {"key":"value2"}
117
+ ```
118
+
119
+ Client usage:
120
+ ```typescript
121
+ const response = await fetch('/user/notifications/watch-notifications/1')
122
+ const reader = response.body.getReader()
123
+ const decoder = new TextDecoder()
124
+
125
+ while (true) {
126
+ const { done, value } = await reader.read()
127
+ if (done) break
128
+ const lines = decoder.decode(value).split('\n')
129
+ for (const line of lines) {
130
+ if (line) console.log(JSON.parse(line))
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ ### Builder Config
138
+
139
+ ```typescript
140
+ interface HonoStreamAppBuilderConfig {
141
+ app?: Hono // Use existing Hono instance
142
+ pathPrefix?: string // Prefix for all routes
143
+ defaultStreamMode?: 'sse' | 'text' // Default: 'sse'
144
+ onRequestStart?: (c: Context) => void
145
+ onRequestEnd?: (c: Context) => void
146
+ onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context) => void
147
+ onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context) => void
148
+ onStreamError?: (procedure, c, error) => Response | Promise<Response>
149
+ }
150
+ ```
151
+
152
+ ### Per-Factory Options
153
+
154
+ ```typescript
155
+ builder.register(factory, context, {
156
+ streamMode: 'text', // Override default mode for this factory
157
+ extendProcedureDoc: ({ base, procedure }) => ({
158
+ summary: 'Custom documentation',
159
+ tags: ['streaming'],
160
+ }),
161
+ })
162
+ ```
163
+
164
+ ## Lifecycle Hooks
165
+
166
+ Hooks execute in the following order:
167
+
168
+ ```
169
+ onRequestStart → onStreamStart → [yields...] → onStreamEnd → onRequestEnd
170
+
171
+ (on error)
172
+
173
+ error sent in stream
174
+
175
+ onStreamEnd
176
+ ```
177
+
178
+ ## Error Handling
179
+
180
+ Errors during streaming are sent as special messages:
181
+
182
+ **SSE Mode:**
183
+ ```
184
+ event: error
185
+ data: {"error":"Error message"}
186
+ ```
187
+
188
+ **Text Mode:**
189
+ ```
190
+ {"error":"Error message"}
191
+ ```
192
+
193
+ For errors before streaming starts (e.g., context resolution), use `onStreamError`:
194
+
195
+ ```typescript
196
+ const builder = new HonoStreamAppBuilder({
197
+ onStreamError: (procedure, c, error) => {
198
+ return c.json({ error: error.message }, 500)
199
+ },
200
+ })
201
+ ```
202
+
203
+ ## Route Documentation
204
+
205
+ Access generated documentation via `builder.docs`:
206
+
207
+ ```typescript
208
+ interface StreamHttpRouteDoc {
209
+ name: string
210
+ path: string
211
+ methods: ('get' | 'post')[]
212
+ streamMode: 'sse' | 'text'
213
+ scope: string | string[]
214
+ version: number
215
+ jsonSchema: {
216
+ params?: object // From schema.params
217
+ yieldType?: object // From schema.yieldType
218
+ returnType?: object // From schema.returnType
219
+ }
220
+ }
221
+ ```
222
+
223
+ ## Procedure Filtering
224
+
225
+ Only streaming procedures (created with `CreateStream`) are registered. Regular procedures created with `Create` are ignored by this builder.
226
+
227
+ ```typescript
228
+ const RPC = Procedures<Context, RPCConfig>()
229
+
230
+ // This will NOT be registered by HonoStreamAppBuilder
231
+ RPC.Create('GetUser', config, async (ctx) => ({ user: 'data' }))
232
+
233
+ // This WILL be registered
234
+ RPC.CreateStream('WatchUser', config, async function* (ctx) {
235
+ yield { update: 'data' }
236
+ })
237
+ ```
238
+
239
+ ## Client Disconnect Handling
240
+
241
+ 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:
242
+
243
+ ```typescript
244
+ RPC.CreateStream('LongStream', config, async function* (ctx) {
245
+ while (!ctx.signal.aborted) {
246
+ yield { tick: Date.now() }
247
+ await new Promise((r) => setTimeout(r, 1000))
248
+ }
249
+ })
250
+ ```
251
+
252
+ ## TypeScript Types
253
+
254
+ ```typescript
255
+ import {
256
+ HonoStreamAppBuilder,
257
+ HonoStreamAppBuilderConfig,
258
+ StreamHttpRouteDoc,
259
+ StreamMode,
260
+ } from 'ts-procedures/implementations/http/hono-stream'
261
+ ```