ts-procedures 5.7.2 → 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 -1051
- package/agent_config/claude-code/skills/guide/api-reference.md +21 -16
- package/agent_config/claude-code/skills/guide/patterns.md +3 -1
- package/agent_config/copilot/copilot-instructions.md +7 -5
- package/agent_config/cursor/cursorrules +7 -5
- package/build/codegen/bin/cli.d.ts +2 -0
- package/build/codegen/bin/cli.js +21 -10
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +44 -2
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/emit-errors.d.ts +4 -1
- package/build/codegen/emit-errors.js +11 -5
- package/build/codegen/emit-errors.js.map +1 -1
- package/build/codegen/emit-errors.test.js +37 -0
- package/build/codegen/emit-errors.test.js.map +1 -1
- package/build/codegen/emit-index.d.ts +3 -1
- package/build/codegen/emit-index.js +6 -13
- package/build/codegen/emit-index.js.map +1 -1
- package/build/codegen/emit-index.test.js +23 -0
- package/build/codegen/emit-index.test.js.map +1 -1
- package/build/codegen/emit-scope.js +17 -13
- 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/build/codegen/index.d.ts +1 -0
- package/build/codegen/index.js +1 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/naming.d.ts +7 -0
- package/build/codegen/naming.js +21 -0
- package/build/codegen/naming.js.map +1 -0
- package/build/codegen/naming.test.d.ts +1 -0
- package/build/codegen/naming.test.js +40 -0
- package/build/codegen/naming.test.js.map +1 -0
- package/build/codegen/pipeline.d.ts +1 -0
- package/build/codegen/pipeline.js +7 -3
- package/build/codegen/pipeline.js.map +1 -1
- package/build/codegen/pipeline.test.js +60 -0
- package/build/codegen/pipeline.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
package/README.md
CHANGED
|
@@ -40,1063 +40,19 @@ const user = await GetUser({}, { userId: '123' })
|
|
|
40
40
|
const user2 = await procedure({}, { userId: '456' })
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Features
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
- **[Core Procedures](docs/core.md)** — Type-safe procedure definitions with `Procedures()`, `Create`, and `CreateStream`. Includes schema validation (Suretype / TypeBox), error handling, generics, testing patterns, and the full API reference.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
- **[Streaming](docs/streaming.md)** — Async generator procedures with yield validation, abort signal integration, SSE examples, and stream error handling.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
Procedures<TContext, TExtendedConfig>(builder?: {
|
|
51
|
-
onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig>) => void
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
| Parameter | Description |
|
|
56
|
-
|-----------|----------------------------------------------------------------------------|
|
|
57
|
-
| `TContext` | The base context type passed to all handlers as the first parameter |
|
|
58
|
-
| `TExtendedConfig` | Additional configuration properties for all procedures `config` properties |
|
|
59
|
-
| `builder.onCreate` | Optional callback invoked when each procedure is registered (runtime) |
|
|
60
|
-
|
|
61
|
-
### Create Function
|
|
62
|
-
|
|
63
|
-
The `Create` function defines individual procedures:
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
Create(name, config, handler)
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Returns:**
|
|
70
|
-
- `{ [name]: handler }` - Named export for the handler
|
|
71
|
-
- `procedure` - Generic reference to the handler
|
|
72
|
-
- `info` - Procedure meta (name, description, schema, `TExtendedConfig` properties, etc.)
|
|
73
|
-
|
|
74
|
-
### Structured Input with schema.input
|
|
75
|
-
|
|
76
|
-
For HTTP APIs and other multi-channel transports, `schema.input` provides per-channel type safety. Each key is an independently validated input channel:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
const { Create } = Procedures<AppContext, APIConfig>()
|
|
80
|
-
|
|
81
|
-
const { UpdateUser } = Create(
|
|
82
|
-
'UpdateUser',
|
|
83
|
-
{
|
|
84
|
-
path: '/users/:id',
|
|
85
|
-
method: 'put',
|
|
86
|
-
schema: {
|
|
87
|
-
input: {
|
|
88
|
-
pathParams: Type.Object({ id: Type.String() }),
|
|
89
|
-
query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
|
|
90
|
-
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
91
|
-
},
|
|
92
|
-
returnType: Type.Object({ ok: Type.Boolean() }),
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
async (ctx, { pathParams, query, body }) => {
|
|
96
|
-
// Each channel is independently typed and validated
|
|
97
|
-
await updateUser(pathParams.id, body)
|
|
98
|
-
if (query.notify) await sendNotification(pathParams.id)
|
|
99
|
-
return { ok: true }
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Rules:**
|
|
105
|
-
- `schema.input` and `schema.params` are **mutually exclusive** — defining both throws `ProcedureRegistrationError`
|
|
106
|
-
- Each channel is validated independently with per-channel error messages
|
|
107
|
-
- Works with both `Create` and `CreateStream`
|
|
108
|
-
|
|
109
|
-
### CreateStream Function
|
|
110
|
-
|
|
111
|
-
The `CreateStream` function defines streaming procedures that yield values over time using async generators:
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
CreateStream(name, config, handler)
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Config Options:**
|
|
118
|
-
- `schema.params` - Input parameter schema (validated at runtime)
|
|
119
|
-
- `schema.yieldType` - Schema for each yielded value (validated if `validateYields: true`)
|
|
120
|
-
- `schema.returnType` - Schema for final return value (documentation only)
|
|
121
|
-
- `validateYields` - Enable runtime validation of yielded values (default: `false`)
|
|
122
|
-
|
|
123
|
-
**Handler Signature:**
|
|
124
|
-
```typescript
|
|
125
|
-
async function* (ctx, params) => AsyncGenerator<TYield, TReturn | void>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Context Extensions (all handlers):**
|
|
129
|
-
- `ctx.error(message, meta?)` - Create a ProcedureError
|
|
130
|
-
- `ctx.signal?` - AbortSignal for cancellation support (optional for `Create`, always present for `CreateStream`)
|
|
131
|
-
|
|
132
|
-
When using the built-in HTTP implementations (Hono, Express), `ctx.signal` is automatically injected from the HTTP request, so handlers can detect client disconnection. For direct usage without a server, `signal` is `undefined` unless you pass one in context.
|
|
133
|
-
|
|
134
|
-
**Returns:**
|
|
135
|
-
- `{ [name]: handler }` - Named generator export
|
|
136
|
-
- `procedure` - Generic reference to the generator
|
|
137
|
-
- `info` - Procedure meta with `isStream: true`
|
|
138
|
-
|
|
139
|
-
## Using Generics
|
|
140
|
-
|
|
141
|
-
### Base Context
|
|
142
|
-
|
|
143
|
-
Define a shared context type for all procedures in your application:
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
interface AppContext {
|
|
147
|
-
authToken: string
|
|
148
|
-
requestId: string
|
|
149
|
-
logger: Logger
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const { Create } = Procedures<AppContext>()
|
|
153
|
-
|
|
154
|
-
const { SecureEndpoint } = Create(
|
|
155
|
-
'SecureEndpoint',
|
|
156
|
-
{},
|
|
157
|
-
async (ctx, params) => {
|
|
158
|
-
// ctx.authToken is typed as string
|
|
159
|
-
// ctx.requestId is typed as string
|
|
160
|
-
// ctx.logger is typed as Logger
|
|
161
|
-
return { token: ctx.authToken }
|
|
162
|
-
},
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
// When calling, you must provide the context
|
|
166
|
-
await SecureEndpoint({ authToken: 'abc', requestId: '123', logger: myLogger }, {})
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Extended Configuration
|
|
170
|
-
|
|
171
|
-
Add custom properties to all procedure configs:
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
interface ExtendedConfig {
|
|
175
|
-
permissions: string[]
|
|
176
|
-
rateLimit?: number
|
|
177
|
-
cacheTTL?: number
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const { Create } = Procedures<AppContext, ExtendedConfig>()
|
|
181
|
-
|
|
182
|
-
const { AdminOnly } = Create(
|
|
183
|
-
'AdminOnly',
|
|
184
|
-
{
|
|
185
|
-
permissions: ['admin'], // Required by ExtendedConfig
|
|
186
|
-
rateLimit: 100, // Optional
|
|
187
|
-
description: 'Admin-only endpoint',
|
|
188
|
-
},
|
|
189
|
-
async (ctx, params) => {
|
|
190
|
-
return { admin: true }
|
|
191
|
-
},
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
// Access extended config via info
|
|
195
|
-
console.log(AdminOnly.info.permissions) // ['admin']
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Combined Example
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
interface CustomContext {
|
|
202
|
-
authToken: string
|
|
203
|
-
tenantId: string
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
interface ExtendedConfig {
|
|
207
|
-
requiresAuth: boolean
|
|
208
|
-
auditLog?: boolean
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const { Create, getProcedures } = Procedures<CustomContext, ExtendedConfig>({
|
|
212
|
-
onCreate: (procedure) => {
|
|
213
|
-
// Register with your framework
|
|
214
|
-
console.log(`Registered: ${procedure.name}`)
|
|
215
|
-
console.log(`Requires Auth: ${procedure.config.requiresAuth}`)
|
|
216
|
-
},
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const { CreateUser } = Create(
|
|
220
|
-
'CreateUser',
|
|
221
|
-
{
|
|
222
|
-
requiresAuth: true,
|
|
223
|
-
auditLog: true,
|
|
224
|
-
description: 'Creates a new user',
|
|
225
|
-
schema: {
|
|
226
|
-
params: Type.Object({
|
|
227
|
-
email: Type.String(),
|
|
228
|
-
name: Type.String(),
|
|
229
|
-
}),
|
|
230
|
-
returnType: Type.Object({ id: Type.String() }),
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
async (ctx, params) => {
|
|
234
|
-
// Both context and params are fully typed
|
|
235
|
-
return { id: 'user-123' }
|
|
236
|
-
},
|
|
237
|
-
)
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Schema Validation
|
|
241
|
-
|
|
242
|
-
### Suretype
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
import { v } from 'suretype'
|
|
246
|
-
|
|
247
|
-
Create(
|
|
248
|
-
'CreatePost',
|
|
249
|
-
{
|
|
250
|
-
schema: {
|
|
251
|
-
params: Type.Object({
|
|
252
|
-
title: Type.String(),
|
|
253
|
-
content: Type.String(),
|
|
254
|
-
tags: Type.array(Type.String()),
|
|
255
|
-
}),
|
|
256
|
-
returnType: Type.Object({
|
|
257
|
-
id: Type.String(),
|
|
258
|
-
createdAt: Type.String(),
|
|
259
|
-
}),
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
async (ctx, params) => {
|
|
263
|
-
// params typed as { title: string, content: string, tags?: string[] }
|
|
264
|
-
return { id: '1', createdAt: new Date().toISOString() }
|
|
265
|
-
},
|
|
266
|
-
)
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### TypeBox
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
import { Type } from 'typebox'
|
|
273
|
-
|
|
274
|
-
Create(
|
|
275
|
-
'CreatePost',
|
|
276
|
-
{
|
|
277
|
-
schema: {
|
|
278
|
-
params: Type.Object({
|
|
279
|
-
title: Type.String(),
|
|
280
|
-
content: Type.String(),
|
|
281
|
-
tags: Type.Optional(Type.Array(Type.String())),
|
|
282
|
-
}),
|
|
283
|
-
returnType: Type.Object({
|
|
284
|
-
id: Type.String(),
|
|
285
|
-
createdAt: Type.String(),
|
|
286
|
-
}),
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
async (ctx, params) => {
|
|
290
|
-
// params typed as { title: string, content: string, tags?: string[] }
|
|
291
|
-
return { id: '1', createdAt: new Date().toISOString() }
|
|
292
|
-
},
|
|
293
|
-
)
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Validation Behavior
|
|
297
|
-
|
|
298
|
-
AJV is configured with:
|
|
299
|
-
- `allErrors: true` - Report all validation errors
|
|
300
|
-
- `coerceTypes: true` - Automatically coerce types when possible
|
|
301
|
-
- `removeAdditional: true` - Strip properties not in schema
|
|
302
|
-
|
|
303
|
-
**Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
|
|
304
|
-
|
|
305
|
-
## Streaming Procedures
|
|
306
|
-
|
|
307
|
-
Streaming procedures use async generators to yield values over time, enabling SSE (Server-Sent Events), HTTP streaming, and real-time data feeds.
|
|
308
|
-
|
|
309
|
-
### Basic Streaming
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { Procedures } from 'ts-procedures'
|
|
313
|
-
import { v } from 'suretype'
|
|
314
|
-
|
|
315
|
-
const { CreateStream } = Procedures<{ userId: string }>()
|
|
316
|
-
|
|
317
|
-
const { StreamUpdates } = CreateStream(
|
|
318
|
-
'StreamUpdates',
|
|
319
|
-
{
|
|
320
|
-
description: 'Stream real-time updates',
|
|
321
|
-
schema: {
|
|
322
|
-
params: v.object({ topic: v.string().required() }),
|
|
323
|
-
yieldType: v.object({
|
|
324
|
-
id: v.string().required(),
|
|
325
|
-
message: v.string().required(),
|
|
326
|
-
timestamp: v.number().required(),
|
|
327
|
-
}),
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
async function* (ctx, params) {
|
|
331
|
-
// Types are inferred from schema:
|
|
332
|
-
// - params.topic: string
|
|
333
|
-
// - yield value must match { id, message, timestamp }
|
|
334
|
-
// - ctx.signal: AbortSignal for cancellation
|
|
335
|
-
|
|
336
|
-
let counter = 0
|
|
337
|
-
while (!ctx.signal.aborted) {
|
|
338
|
-
yield {
|
|
339
|
-
id: `${counter++}`,
|
|
340
|
-
message: `Update for ${params.topic}`,
|
|
341
|
-
timestamp: Date.now(),
|
|
342
|
-
}
|
|
343
|
-
await new Promise(r => setTimeout(r, 1000))
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
// Consume the stream
|
|
349
|
-
for await (const update of StreamUpdates({ userId: 'user-123' }, { topic: 'news' })) {
|
|
350
|
-
console.log(update.message)
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Yield Validation
|
|
355
|
-
|
|
356
|
-
By default, yielded values are not validated for performance. Enable validation with `validateYields: true`:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
const { ValidatedStream } = CreateStream(
|
|
360
|
-
'ValidatedStream',
|
|
361
|
-
{
|
|
362
|
-
schema: {
|
|
363
|
-
yieldType: v.object({ count: v.number().required() }),
|
|
364
|
-
},
|
|
365
|
-
validateYields: true, // Enable runtime validation of each yield
|
|
366
|
-
},
|
|
367
|
-
async function* () {
|
|
368
|
-
yield { count: 1 } // Valid
|
|
369
|
-
yield { count: 2 } // Valid
|
|
370
|
-
// yield { count: 'invalid' } // Would throw ProcedureYieldValidationError
|
|
371
|
-
},
|
|
372
|
-
)
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Abort Signal Integration
|
|
376
|
-
|
|
377
|
-
#### Streaming Procedures
|
|
378
|
-
|
|
379
|
-
The `ctx.signal` allows stream handlers to detect when consumers stop iterating. After completion, `signal.reason` indicates why the stream ended:
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
const { CancellableStream } = CreateStream(
|
|
383
|
-
'CancellableStream',
|
|
384
|
-
{},
|
|
385
|
-
async function* (ctx) {
|
|
386
|
-
try {
|
|
387
|
-
while (!ctx.signal.aborted) {
|
|
388
|
-
yield await fetchNextItem()
|
|
389
|
-
}
|
|
390
|
-
} finally {
|
|
391
|
-
// Distinguish normal completion from client disconnect
|
|
392
|
-
if (ctx.signal.reason === 'stream-completed') {
|
|
393
|
-
// Stream finished normally
|
|
394
|
-
} else {
|
|
395
|
-
// Client disconnected or external abort
|
|
396
|
-
}
|
|
397
|
-
await cleanup()
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
// Consumer can break early - signal.aborted becomes true
|
|
403
|
-
for await (const item of CancellableStream({}, {})) {
|
|
404
|
-
if (shouldStop) break // Triggers abort
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
#### Regular Procedures
|
|
409
|
-
|
|
410
|
-
For regular procedures, `ctx.signal` is available when the server implementation provides it. The built-in HTTP integrations (Hono RPC, Express RPC) inject the request's abort signal automatically:
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
const { Create } = Procedures<{ signal: AbortSignal }>()
|
|
414
|
-
|
|
415
|
-
const { LongQuery } = Create(
|
|
416
|
-
'LongQuery',
|
|
417
|
-
{},
|
|
418
|
-
async (ctx, params) => {
|
|
419
|
-
// Pass signal to downstream operations
|
|
420
|
-
const result = await fetch('https://api.example.com/data', {
|
|
421
|
-
signal: ctx.signal,
|
|
422
|
-
})
|
|
423
|
-
return result.json()
|
|
424
|
-
},
|
|
425
|
-
)
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
When using the Hono or Express implementations, `ctx.signal` aborts when the client disconnects, automatically cancelling in-flight `fetch()` calls, database queries, or any other signal-aware operation.
|
|
429
|
-
|
|
430
|
-
### SSE Integration Example
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
import express from 'express'
|
|
434
|
-
import { Procedures } from 'ts-procedures'
|
|
435
|
-
|
|
436
|
-
const app = express()
|
|
437
|
-
|
|
438
|
-
const { CreateStream, getProcedures } = Procedures<{ req: express.Request }>({
|
|
439
|
-
onCreate: (proc) => {
|
|
440
|
-
if (proc.isStream) {
|
|
441
|
-
// Register streaming procedures as SSE endpoints
|
|
442
|
-
app.get(`/stream/${proc.name}`, async (req, res) => {
|
|
443
|
-
res.writeHead(200, {
|
|
444
|
-
'Content-Type': 'text/event-stream',
|
|
445
|
-
'Cache-Control': 'no-cache',
|
|
446
|
-
'Connection': 'keep-alive',
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
const generator = proc.handler({ req }, req.query)
|
|
450
|
-
|
|
451
|
-
req.on('close', async () => {
|
|
452
|
-
// Client disconnected - stop the generator
|
|
453
|
-
await generator.return(undefined)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
try {
|
|
457
|
-
for await (const data of generator) {
|
|
458
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`)
|
|
459
|
-
}
|
|
460
|
-
} finally {
|
|
461
|
-
res.end()
|
|
462
|
-
}
|
|
463
|
-
})
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
// Define a streaming procedure
|
|
469
|
-
CreateStream(
|
|
470
|
-
'LiveFeed',
|
|
471
|
-
{
|
|
472
|
-
schema: {
|
|
473
|
-
params: v.object({ channel: v.string() }),
|
|
474
|
-
yieldType: v.object({ event: v.string(), data: v.any() }),
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
async function* (ctx, params) {
|
|
478
|
-
while (!ctx.signal.aborted) {
|
|
479
|
-
const event = await pollForEvent(params.channel)
|
|
480
|
-
yield event
|
|
481
|
-
}
|
|
482
|
-
},
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
app.listen(3000)
|
|
486
|
-
// SSE endpoint: GET /stream/LiveFeed?channel=updates
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Stream Errors
|
|
490
|
-
|
|
491
|
-
Streaming procedures support the same error handling as regular procedures:
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
const { StreamWithErrors } = CreateStream(
|
|
495
|
-
'StreamWithErrors',
|
|
496
|
-
{},
|
|
497
|
-
async function* (ctx) {
|
|
498
|
-
yield { status: 'starting' }
|
|
499
|
-
|
|
500
|
-
const data = await fetchData()
|
|
501
|
-
if (!data) {
|
|
502
|
-
throw ctx.error('No data available', { code: 'NO_DATA' })
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
yield { status: 'complete', data }
|
|
506
|
-
},
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
for await (const item of StreamWithErrors({}, {})) {
|
|
511
|
-
console.log(item)
|
|
512
|
-
}
|
|
513
|
-
} catch (e) {
|
|
514
|
-
if (e instanceof ProcedureError) {
|
|
515
|
-
console.log(e.message) // 'No data available'
|
|
516
|
-
console.log(e.meta) // { code: 'NO_DATA' }
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Error Handling
|
|
522
|
-
|
|
523
|
-
### Using ctx.error()
|
|
524
|
-
|
|
525
|
-
The `error()` function is injected into both hooks and handlers:
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
Create(
|
|
529
|
-
'GetResource',
|
|
530
|
-
{},
|
|
531
|
-
async (ctx, params) => {
|
|
532
|
-
const resource = await db.find(params.id)
|
|
533
|
-
if (!resource) {
|
|
534
|
-
throw ctx.error(404, 'Resource not found', { id: params.id })
|
|
535
|
-
}
|
|
536
|
-
return resource
|
|
537
|
-
},
|
|
538
|
-
)
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
### Error Handling
|
|
542
|
-
|
|
543
|
-
| Error Class | Trigger |
|
|
544
|
-
|-------------|---------|
|
|
545
|
-
| ProcedureError | `ctx.error()` in handlers |
|
|
546
|
-
| ProcedureValidationError | Schema validation failure (params) |
|
|
547
|
-
| ProcedureYieldValidationError | Yield validation failure (streaming with `validateYields: true`) |
|
|
548
|
-
| ProcedureRegistrationError | Invalid schema at registration |
|
|
549
|
-
|
|
550
|
-
### Error Properties
|
|
551
|
-
|
|
552
|
-
```typescript
|
|
553
|
-
try {
|
|
554
|
-
await MyProcedure(ctx, params)
|
|
555
|
-
} catch (e) {
|
|
556
|
-
if (e instanceof ProcedureError) {
|
|
557
|
-
console.log(e.procedureName) // 'MyProcedure'
|
|
558
|
-
console.log(e.message) // 'Resource not found'
|
|
559
|
-
console.log(e.meta) // { id: '123' }
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
## Framework Integration
|
|
565
|
-
|
|
566
|
-
### onCreate Callback
|
|
567
|
-
|
|
568
|
-
Register procedures with your framework (Express, Fastify, etc.):
|
|
49
|
+
- **[HTTP Integrations](docs/http-integrations.md)** — Express and Hono builders with lifecycle hooks, route documentation, `DocRegistry` for composing docs, and per-channel input validation.
|
|
569
50
|
|
|
570
|
-
|
|
571
|
-
import express from 'express'
|
|
572
|
-
|
|
573
|
-
const app = express()
|
|
574
|
-
const routes: Map<string, Function> = new Map()
|
|
575
|
-
|
|
576
|
-
const { Create } = Procedures<{ req: Request; res: Response }>({
|
|
577
|
-
onCreate: ({ name, handler, config }) => {
|
|
578
|
-
// Register as Express route
|
|
579
|
-
app.post(`/rpc/${name}`, async (req, res) => {
|
|
580
|
-
try {
|
|
581
|
-
const result = await handler({ req, res }, req.body)
|
|
582
|
-
res.json(result)
|
|
583
|
-
} catch (e) {
|
|
584
|
-
if (e instanceof ProcedureError) {
|
|
585
|
-
res.status(500).json({ error: e.message })
|
|
586
|
-
} else {
|
|
587
|
-
res.status(500).json({ error: 'Internal error' })
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
})
|
|
591
|
-
},
|
|
592
|
-
})
|
|
593
|
-
|
|
594
|
-
// Procedures are automatically registered as /rpc/GetUser, /rpc/CreateUser, etc.
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### Express RPC Integration
|
|
598
|
-
|
|
599
|
-
`ts-procedures` includes an RPC-style HTTP integration for Express that creates POST routes at `/rpc/{name}/{version}` paths with automatic JSON schema documentation.
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
603
|
-
|
|
604
|
-
// Create procedure factory with RPC config
|
|
605
|
-
const RPC = Procedures<AppContext, RPCConfig>()
|
|
606
|
-
|
|
607
|
-
// Define procedures with name and version
|
|
608
|
-
RPC.Create(
|
|
609
|
-
'GetUser',
|
|
610
|
-
{
|
|
611
|
-
name: ['users', 'get'],
|
|
612
|
-
version: 1,
|
|
613
|
-
schema: {
|
|
614
|
-
params: Type.Object({ id: Type.String() }),
|
|
615
|
-
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
616
|
-
},
|
|
617
|
-
},
|
|
618
|
-
async (ctx, params) => {
|
|
619
|
-
return { id: params.id, name: 'John Doe' }
|
|
620
|
-
}
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
// Build Express app with registered procedures
|
|
624
|
-
const app = new ExpressRPCAppBuilder()
|
|
625
|
-
.register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
|
|
626
|
-
.build()
|
|
627
|
-
|
|
628
|
-
app.listen(3000)
|
|
629
|
-
// Route created: POST /rpc/users/get/1
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
See [Express RPC Integration Guide](src/implementations/http/express-rpc/README.md) for complete setup instructions including lifecycle hooks, error handling, and route documentation.
|
|
633
|
-
|
|
634
|
-
### Hono API Integration
|
|
635
|
-
|
|
636
|
-
`ts-procedures` includes a REST-style HTTP integration for Hono that routes by HTTP method with per-channel input validation via `schema.input`.
|
|
637
|
-
|
|
638
|
-
```typescript
|
|
639
|
-
import { Procedures } from 'ts-procedures'
|
|
640
|
-
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
641
|
-
import type { APIConfig } from 'ts-procedures/http'
|
|
642
|
-
import { Type } from 'typebox'
|
|
643
|
-
|
|
644
|
-
const API = Procedures<{ userId: string }, APIConfig>()
|
|
645
|
-
|
|
646
|
-
API.Create('GetUser', {
|
|
647
|
-
path: '/users/:id',
|
|
648
|
-
method: 'get',
|
|
649
|
-
schema: {
|
|
650
|
-
input: {
|
|
651
|
-
pathParams: Type.Object({ id: Type.String() }),
|
|
652
|
-
},
|
|
653
|
-
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
654
|
-
},
|
|
655
|
-
}, async (ctx, { pathParams }) => {
|
|
656
|
-
return await fetchUser(pathParams.id)
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
API.Create('CreateUser', {
|
|
660
|
-
path: '/users',
|
|
661
|
-
method: 'post',
|
|
662
|
-
schema: {
|
|
663
|
-
input: {
|
|
664
|
-
body: Type.Object({ name: Type.String(), email: Type.String() }),
|
|
665
|
-
},
|
|
666
|
-
},
|
|
667
|
-
}, async (ctx, { body }) => {
|
|
668
|
-
return await createUser(body)
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
|
|
672
|
-
.register(API, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
|
|
673
|
-
.build()
|
|
674
|
-
|
|
675
|
-
// Routes:
|
|
676
|
-
// GET /api/users/:id → 200
|
|
677
|
-
// POST /api/users → 201
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
See [Hono API Integration Guide](src/implementations/http/hono-api/) for complete setup.
|
|
681
|
-
|
|
682
|
-
### Introspection with getProcedures()
|
|
683
|
-
|
|
684
|
-
Access all registered procedures for documentation or routing:
|
|
685
|
-
|
|
686
|
-
```typescript
|
|
687
|
-
const { Create, getProcedures } = Procedures()
|
|
688
|
-
|
|
689
|
-
Create('GetUser', { schema: { params: Type.Object({ id: Type.String() }) } }, async () => {})
|
|
690
|
-
Create('ListUsers', { schema: { params: Type.Object({}) } }, async () => {})
|
|
691
|
-
|
|
692
|
-
// Get all registered procedures
|
|
693
|
-
const procedures = getProcedures()
|
|
694
|
-
|
|
695
|
-
// Generate OpenAPI spec
|
|
696
|
-
for (const config of procedures) {
|
|
697
|
-
console.log(`${config.name}:`, config.schema)
|
|
698
|
-
}
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
### DocRegistry — Composing Docs from Multiple Builders
|
|
702
|
-
|
|
703
|
-
Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope:
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
707
|
-
|
|
708
|
-
const docs = new DocRegistry({
|
|
709
|
-
basePath: '/api',
|
|
710
|
-
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
711
|
-
errors: DocRegistry.defaultErrors(),
|
|
712
|
-
})
|
|
713
|
-
.from(rpcBuilder)
|
|
714
|
-
.from(apiBuilder)
|
|
715
|
-
.from(streamBuilder)
|
|
716
|
-
|
|
717
|
-
app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
`from()` stores a reference — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`. Supports optional `filter` and `transform` options for customizing output.
|
|
721
|
-
|
|
722
|
-
## Testing
|
|
723
|
-
|
|
724
|
-
Procedures return handlers that can be called directly in tests:
|
|
725
|
-
|
|
726
|
-
```typescript
|
|
727
|
-
import { describe, test, expect } from 'vitest'
|
|
728
|
-
import { Procedures } from 'ts-procedures'
|
|
729
|
-
import { Type } from 'typebox'
|
|
730
|
-
|
|
731
|
-
interface MyCustomContext {
|
|
732
|
-
userId?: string
|
|
733
|
-
userName?: string
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const { Create } = Procedures<MyCustomContext>()
|
|
737
|
-
|
|
738
|
-
const { GetUser, info } = Create(
|
|
739
|
-
'GetUser',
|
|
740
|
-
{
|
|
741
|
-
schema: {
|
|
742
|
-
params: Type.Object({ hideName: Type.Optional(Type.Boolean()) }),
|
|
743
|
-
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
744
|
-
},
|
|
745
|
-
},
|
|
746
|
-
async (ctx, params) => {
|
|
747
|
-
if (!params.userName || !ctx.userId) {
|
|
748
|
-
throw ctx.error('User is not authenticated')
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
id: params.userId,
|
|
753
|
-
name: params?.hideName ? '*******' : params.userName
|
|
754
|
-
}
|
|
755
|
-
},
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
describe('GetUser', () => {
|
|
759
|
-
test('returns user', async () => {
|
|
760
|
-
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: false })
|
|
761
|
-
expect(result).toEqual({ id: '123', name: 'Ray' })
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
test('hides user name', async () => {
|
|
765
|
-
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: true })
|
|
766
|
-
expect(result).toEqual({ id: '123', name: '*******' })
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
test('validates params', async () => {
|
|
770
|
-
await expect(GetUser({}, {})).rejects.toThrow(ProcedureValidationError)
|
|
771
|
-
})
|
|
772
|
-
|
|
773
|
-
test('has correct schema', () => {
|
|
774
|
-
expect(info.schema.params).toEqual({
|
|
775
|
-
type: 'object',
|
|
776
|
-
properties: { id: { type: 'string' } },
|
|
777
|
-
required: ['id'],
|
|
778
|
-
})
|
|
779
|
-
})
|
|
780
|
-
})
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
## API Reference
|
|
784
|
-
|
|
785
|
-
### Procedures(builder?)
|
|
786
|
-
|
|
787
|
-
Creates a procedure factory.
|
|
788
|
-
|
|
789
|
-
**Parameters:**
|
|
790
|
-
- `builder.onCreate` - Callback invoked when each procedure is registered
|
|
791
|
-
|
|
792
|
-
**Returns:**
|
|
793
|
-
- `Create` - Function to define procedures
|
|
794
|
-
- `getProcedures()` - Returns `Array` of all registered procedures
|
|
795
|
-
|
|
796
|
-
### Create(name, config, handler)
|
|
797
|
-
|
|
798
|
-
Defines a procedure.
|
|
799
|
-
|
|
800
|
-
**Parameters:**
|
|
801
|
-
- `name` - Unique procedure name (becomes named export)
|
|
802
|
-
- `config.description` - Optional description
|
|
803
|
-
- `config.schema.params` - Suretype or TypeBox schema for params (validated at runtime)
|
|
804
|
-
- `config.schema.returnType` - Suretype or TypeBox schema for return returnType (documentation only)
|
|
805
|
-
- Additional properties from `TExtendedConfig`
|
|
806
|
-
- `handler` - Async function `(ctx, params) => Promise<returnType>`
|
|
807
|
-
|
|
808
|
-
**Returns:**
|
|
809
|
-
- `{ [name]: handler }` - Named handler export
|
|
810
|
-
- `procedure` - Generic handler reference
|
|
811
|
-
- `info` - Procedure metareturnType
|
|
812
|
-
|
|
813
|
-
### Type Exports
|
|
814
|
-
|
|
815
|
-
```typescript
|
|
816
|
-
import {
|
|
817
|
-
// Core
|
|
818
|
-
Procedures,
|
|
819
|
-
|
|
820
|
-
// Errors
|
|
821
|
-
ProcedureError,
|
|
822
|
-
ProcedureValidationError,
|
|
823
|
-
ProcedureRegistrationError,
|
|
824
|
-
ProcedureYieldValidationError, // For streaming yield validation
|
|
825
|
-
|
|
826
|
-
// Types
|
|
827
|
-
TLocalContext,
|
|
828
|
-
TStreamContext, // Streaming context (AbortSignal always present)
|
|
829
|
-
TProcedureRegistration,
|
|
830
|
-
TStreamProcedureRegistration, // Streaming procedure registration
|
|
831
|
-
TNoContextProvided,
|
|
832
|
-
|
|
833
|
-
// Schema utilities
|
|
834
|
-
extractJsonSchema,
|
|
835
|
-
schemaParser,
|
|
836
|
-
isTypeboxSchema,
|
|
837
|
-
isSuretypeSchema,
|
|
838
|
-
|
|
839
|
-
// Schema types
|
|
840
|
-
TJSONSchema,
|
|
841
|
-
TSchemaLib,
|
|
842
|
-
TSchemaLibGenerator, // AsyncGenerator type utility
|
|
843
|
-
TSchemaParsed,
|
|
844
|
-
TSchemaValidationError,
|
|
845
|
-
Prettify,
|
|
846
|
-
} from 'ts-procedures'
|
|
847
|
-
|
|
848
|
-
// HTTP types
|
|
849
|
-
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
|
|
51
|
+
- **[Client Code Generation](docs/client-and-codegen.md)** — Generate type-safe client SDKs from your server's `DocRegistry`. CLI and programmatic API, adapters, hooks, streaming support, and self-contained mode.
|
|
850
52
|
|
|
851
|
-
|
|
852
|
-
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
853
|
-
import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
|
|
854
|
-
|
|
855
|
-
// Client Runtime
|
|
856
|
-
import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
857
|
-
import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
|
|
858
|
-
|
|
859
|
-
// Code Generation
|
|
860
|
-
import { generateClient } from 'ts-procedures/codegen'
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
## Client Code Generation
|
|
864
|
-
|
|
865
|
-
ts-procedures can generate type-safe client SDKs directly from your server's `DocRegistry` output. Generated files include TypeScript types and callable functions for every registered procedure, organized by scope — no manual type duplication required.
|
|
866
|
-
|
|
867
|
-
### Quick Start
|
|
868
|
-
|
|
869
|
-
**Step 1 — Serve your docs endpoint:**
|
|
870
|
-
|
|
871
|
-
```typescript
|
|
872
|
-
app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
**Step 2 — Generate the client:**
|
|
876
|
-
|
|
877
|
-
```bash
|
|
878
|
-
npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
**Step 3 — Use the client:**
|
|
882
|
-
|
|
883
|
-
```typescript
|
|
884
|
-
import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
885
|
-
import { createScopeBindings } from './generated/api'
|
|
886
|
-
|
|
887
|
-
const client = createClient({
|
|
888
|
-
adapter: createFetchAdapter(),
|
|
889
|
-
basePath: 'http://localhost:3000',
|
|
890
|
-
scopes: createScopeBindings,
|
|
891
|
-
hooks: {
|
|
892
|
-
onBeforeRequest(ctx) {
|
|
893
|
-
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
894
|
-
return ctx
|
|
895
|
-
},
|
|
896
|
-
},
|
|
897
|
-
})
|
|
898
|
-
|
|
899
|
-
// Fully typed — params and response inferred from server schemas
|
|
900
|
-
const user = await client.users.GetUser({ pathParams: { id: '123' } })
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
### Generated File Structure
|
|
904
|
-
|
|
905
|
-
Running the codegen command produces one file per scope, plus shared error types and a barrel export:
|
|
906
|
-
|
|
907
|
-
```
|
|
908
|
-
generated/
|
|
909
|
-
users.ts # Types + callables for "users" scope
|
|
910
|
-
billing.ts # Types + callables for "billing" scope
|
|
911
|
-
notifications.ts # Types + callables for stream procedures
|
|
912
|
-
_errors.ts # Typed error classes + ProcedureErrorUnion
|
|
913
|
-
index.ts # Barrel exports + createScopeBindings
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
### CLI Reference
|
|
917
|
-
|
|
918
|
-
| Flag | Description | Required |
|
|
919
|
-
|------|-------------|----------|
|
|
920
|
-
| `--url <url>` | Fetch DocEnvelope from URL | One of `--url` or `--file` |
|
|
921
|
-
| `--file <path>` | Read DocEnvelope from JSON file | One of `--url` or `--file` |
|
|
922
|
-
| `--out <dir>` | Output directory | Yes |
|
|
923
|
-
| `--watch` | Poll for changes and regenerate | No |
|
|
924
|
-
| `--interval <ms>` | Watch poll interval (default: 3000) | No |
|
|
925
|
-
| `--dry-run` | Preview without writing files | No |
|
|
926
|
-
| `--client-import-path <path>` | Override import path (default: `ts-procedures/client`) | No |
|
|
927
|
-
| `--namespace-types` | Wrap types in nested TypeScript namespaces per scope/route | No |
|
|
928
|
-
| `--config <path>` | Path to config file (default: `ts-procedures-codegen.config.json`) | No |
|
|
929
|
-
| `--enum-style <union\|enum>` | TypeScript enum style (requires `--namespace-types`) | No |
|
|
930
|
-
| `--depluralize` | Singularize array item type names (requires `--namespace-types`) | No |
|
|
931
|
-
| `--array-item-naming <value>` | Postfix for array item type names (requires `--namespace-types`) | No |
|
|
932
|
-
| `--uncountable-words <list>` | Comma-separated words to skip singularization (requires `--namespace-types`) | No |
|
|
933
|
-
| `--jsdoc` | Emit JSDoc comments from JSON Schema descriptions (requires `--namespace-types`) | No |
|
|
934
|
-
| `--self-contained` | Emit `_types.ts` and `_client.ts` — no runtime dependency on `ts-procedures` | No |
|
|
935
|
-
|
|
936
|
-
> **Note:** ajsc formatting options (`--enum-style enum`, `--depluralize`, `--array-item-naming`, `--uncountable-words`, `--jsdoc`) only take effect with `--namespace-types`. In flat mode, all types are inlined and these options have no effect.
|
|
937
|
-
>
|
|
938
|
-
> You can also use a `ts-procedures-codegen.config.json` file in your project root instead of CLI flags. CLI flags override config values.
|
|
939
|
-
|
|
940
|
-
### Adapter Interface
|
|
941
|
-
|
|
942
|
-
The client requires an adapter that handles the actual HTTP transport. A built-in fetch adapter is included, and you can implement your own for any HTTP library:
|
|
943
|
-
|
|
944
|
-
```typescript
|
|
945
|
-
import { createFetchAdapter } from 'ts-procedures/client'
|
|
946
|
-
|
|
947
|
-
// Use the built-in fetch adapter
|
|
948
|
-
const adapter = createFetchAdapter({ headers: { 'X-API-Key': 'my-key' } })
|
|
949
|
-
|
|
950
|
-
// Or implement your own (e.g., for axios)
|
|
951
|
-
const axiosAdapter: ClientAdapter = {
|
|
952
|
-
async request({ url, method, headers, body }) {
|
|
953
|
-
const res = await axios({ url, method, headers, data: body })
|
|
954
|
-
return { status: res.status, headers: res.headers, body: res.data }
|
|
955
|
-
},
|
|
956
|
-
async stream({ url, method, headers, body }) {
|
|
957
|
-
// Return AsyncIterable of SSE events
|
|
958
|
-
},
|
|
959
|
-
}
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
### Hooks
|
|
963
|
-
|
|
964
|
-
Hooks let you intercept requests and responses globally or per-procedure call. Global hooks apply to every call made through the client instance; per-procedure hooks override or extend them for a single invocation.
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
// Global hooks (apply to all calls)
|
|
968
|
-
const client = createClient({
|
|
969
|
-
adapter,
|
|
970
|
-
basePath: 'http://localhost:3000',
|
|
971
|
-
scopes: createScopeBindings,
|
|
972
|
-
hooks: {
|
|
973
|
-
onBeforeRequest(ctx) { /* add auth headers */ return ctx },
|
|
974
|
-
onAfterResponse(ctx) { /* handle errors, logging */ },
|
|
975
|
-
onError(ctx) { /* error reporting */ },
|
|
976
|
-
},
|
|
977
|
-
})
|
|
978
|
-
|
|
979
|
-
// Per-procedure hook override
|
|
980
|
-
await client.users.GetUser({ pathParams: { id: '123' } }, {
|
|
981
|
-
onAfterResponse(ctx) {
|
|
982
|
-
const rateLimit = ctx.response.headers['x-rate-limit-remaining']
|
|
983
|
-
},
|
|
984
|
-
})
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
### Streaming
|
|
988
|
-
|
|
989
|
-
Stream procedures return a `TypedStream` — an async iterable for yield values, with a `.result` promise for the final return value:
|
|
990
|
-
|
|
991
|
-
```typescript
|
|
992
|
-
const stream = client.events.WatchNotifications({ filter: 'all' })
|
|
993
|
-
|
|
994
|
-
for await (const event of stream) {
|
|
995
|
-
console.log(event) // typed as WatchNotificationsYield
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const result = await stream.result // typed as WatchNotificationsReturn
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
### Programmatic API
|
|
1002
|
-
|
|
1003
|
-
For build pipelines or custom tooling, `generateClient` can be called directly without the CLI:
|
|
1004
|
-
|
|
1005
|
-
```typescript
|
|
1006
|
-
import { generateClient } from 'ts-procedures/codegen'
|
|
1007
|
-
|
|
1008
|
-
await generateClient({
|
|
1009
|
-
url: 'http://localhost:3000/docs',
|
|
1010
|
-
outDir: './src/generated/api',
|
|
1011
|
-
clientImportPath: '@my-app/procedures-client', // optional
|
|
1012
|
-
namespaceTypes: true, // optional — wrap types in nested namespaces
|
|
1013
|
-
selfContained: true, // optional — emit _types.ts + _client.ts (no ts-procedures runtime dep)
|
|
1014
|
-
dryRun: false, // optional
|
|
1015
|
-
ajsc: { // optional — ajsc TypescriptConverter options
|
|
1016
|
-
enumStyle: 'union',
|
|
1017
|
-
depluralize: true,
|
|
1018
|
-
arrayItemNaming: 'Item',
|
|
1019
|
-
uncountableWords: ['criteria'],
|
|
1020
|
-
},
|
|
1021
|
-
})
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
### Self-Contained Mode
|
|
1025
|
-
|
|
1026
|
-
With `--self-contained`, the generated output includes two additional files in the output directory:
|
|
1027
|
-
|
|
1028
|
-
- **`_types.ts`** — All client type definitions (`ClientInstance`, `TypedStream`, `ProcedureCallOptions`, hooks, adapters, descriptors)
|
|
1029
|
-
- **`_client.ts`** — Full client runtime: `createClient`, `createFetchAdapter`, hook pipeline, and error classes (`ClientRequestError`, `ClientPathParamError`, `ClientStreamError`)
|
|
1030
|
-
|
|
1031
|
-
All generated scope files and `index.ts` import from `./_types` instead of `ts-procedures/client`, so app consumers can import everything from the generated directory without needing `ts-procedures` as a runtime dependency. `ts-procedures` becomes a devDependency only.
|
|
1032
|
-
|
|
1033
|
-
```typescript
|
|
1034
|
-
// Without --self-contained (default)
|
|
1035
|
-
import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
1036
|
-
|
|
1037
|
-
// With --self-contained
|
|
1038
|
-
import { createClient, createFetchAdapter } from './generated/_client'
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
## AI Agent Setup
|
|
1042
|
-
|
|
1043
|
-
ts-procedures ships with built-in AI assistant configuration for **Claude Code**, **Cursor**, and **GitHub Copilot**. This gives AI tools framework-aware context when writing ts-procedures code in your project.
|
|
1044
|
-
|
|
1045
|
-
### Quick Setup
|
|
1046
|
-
|
|
1047
|
-
```bash
|
|
1048
|
-
npx ts-procedures-setup
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
This installs rules for all supported AI tools. You can also target specific tools:
|
|
1052
|
-
|
|
1053
|
-
```bash
|
|
1054
|
-
npx ts-procedures-setup claude # Claude Code only
|
|
1055
|
-
npx ts-procedures-setup cursor # Cursor only
|
|
1056
|
-
npx ts-procedures-setup copilot # GitHub Copilot only
|
|
1057
|
-
```
|
|
1058
|
-
|
|
1059
|
-
### What Gets Installed
|
|
1060
|
-
|
|
1061
|
-
| Tool | Files | Auto-updates? |
|
|
1062
|
-
|------|-------|---------------|
|
|
1063
|
-
| **Claude Code** | `.claude/rules/ts-procedures.md`, `.claude/commands/ts-procedures-scaffold.md`, `.claude/commands/ts-procedures-review.md`, `.claude/agents/ts-procedures-architect.md` | Yes |
|
|
1064
|
-
| **Cursor** | `.cursorrules` (marker-based section) | Yes |
|
|
1065
|
-
| **GitHub Copilot** | `.github/copilot-instructions.md` (marker-based section) | Yes |
|
|
1066
|
-
|
|
1067
|
-
### Auto-Updates
|
|
1068
|
-
|
|
1069
|
-
After initial setup, rules are automatically refreshed on every `npm install` or `npm update`. When ts-procedures publishes a new version, your AI tools get the latest framework guidance without any manual steps.
|
|
1070
|
-
|
|
1071
|
-
### Claude Code Features
|
|
1072
|
-
|
|
1073
|
-
Once installed, Claude Code gets:
|
|
1074
|
-
|
|
1075
|
-
- **Framework reference** — auto-loaded rules with core API, schema system, error handling, and decision framework
|
|
1076
|
-
- **Scaffold command** — `/project:ts-procedures-scaffold <type> <Name>` generates procedures, streams, and HTTP setups with correct patterns
|
|
1077
|
-
- **Review command** — `/project:ts-procedures-review <path>` checks code against a 60+ item checklist
|
|
1078
|
-
- **Architecture agent** — `ts-procedures-architect` helps plan procedure structure, schema design, and HTTP implementation choices
|
|
1079
|
-
|
|
1080
|
-
### CLI Options
|
|
1081
|
-
|
|
1082
|
-
```bash
|
|
1083
|
-
npx ts-procedures-setup --force # Overwrite without prompting
|
|
1084
|
-
npx ts-procedures-setup --dry-run # Preview what would be created/updated
|
|
1085
|
-
npx ts-procedures-setup --check # Exit with code 1 if files are outdated (for CI)
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
### Gitignore
|
|
1089
|
-
|
|
1090
|
-
The `.claude/` files are auto-generated and regenerated on `npm install`. You can add them to `.gitignore`:
|
|
1091
|
-
|
|
1092
|
-
```gitignore
|
|
1093
|
-
# Auto-generated AI agent rules (regenerated on npm install)
|
|
1094
|
-
.claude/rules/ts-procedures.md
|
|
1095
|
-
.claude/commands/ts-procedures-*.md
|
|
1096
|
-
.claude/agents/ts-procedures-*.md
|
|
1097
|
-
```
|
|
53
|
+
- **[AI Agent Setup](docs/ai-agent-setup.md)** — Built-in configuration for Claude Code, Cursor, and GitHub Copilot. Auto-updates on `npm install`.
|
|
1098
54
|
|
|
1099
|
-
|
|
55
|
+
Full documentation is available on [GitHub](https://github.com/thermsio/ts-procedures).
|
|
1100
56
|
|
|
1101
57
|
## License
|
|
1102
58
|
|