ts-procedures 5.2.0 → 5.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.
@@ -0,0 +1,550 @@
1
+ # ts-procedures API Reference
2
+
3
+ ## Imports
4
+
5
+ ```typescript
6
+ // Core
7
+ import { Procedures } from 'ts-procedures'
8
+ import type {
9
+ TLocalContext,
10
+ TStreamContext,
11
+ TProcedureRegistration,
12
+ TStreamProcedureRegistration,
13
+ } from 'ts-procedures'
14
+
15
+ // Errors
16
+ import {
17
+ ProcedureError,
18
+ ProcedureValidationError,
19
+ ProcedureYieldValidationError,
20
+ ProcedureRegistrationError,
21
+ } from 'ts-procedures'
22
+
23
+ // Schema utilities
24
+ import { extractJsonSchema } from 'ts-procedures'
25
+ import { schemaParser } from 'ts-procedures'
26
+ import { isTypeboxSchema } from 'ts-procedures'
27
+ import type { TSchemaLib, TSchemaLibGenerator, TJSONSchema, TSchemaParsed, TSchemaValidationError } from 'ts-procedures'
28
+
29
+ // Stack utilities
30
+ import { captureDefinitionInfo, formatDefinitionInfo } from 'ts-procedures'
31
+ import type { DefinitionLocation, DefinitionInfo } from 'ts-procedures'
32
+
33
+ // HTTP types (types only, no runtime)
34
+ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
35
+
36
+ // Express RPC
37
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
38
+
39
+ // Hono RPC
40
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
41
+
42
+ // Hono Streaming
43
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Procedures\<TContext, TExtendedConfig\>(builder?)
49
+
50
+ Factory function that creates a scoped procedure registration system.
51
+
52
+ ```typescript
53
+ function Procedures<TContext = unknown, TExtendedConfig = unknown>(
54
+ builder?: {
55
+ onCreate?: (procedure: {
56
+ name: string
57
+ isStream?: boolean
58
+ handler: Function
59
+ config: { description?: string; schema?: object } & TExtendedConfig
60
+ }) => void
61
+ }
62
+ ): {
63
+ Create: CreateFunction
64
+ CreateStream: CreateStreamFunction
65
+ getProcedures(): Array<TProcedureRegistration | TStreamProcedureRegistration>
66
+ getProcedure(name: string): TProcedureRegistration | TStreamProcedureRegistration | undefined
67
+ removeProcedure(name: string): boolean
68
+ clear(): void
69
+ }
70
+ ```
71
+
72
+ ### Parameters
73
+
74
+ - `builder.onCreate` — Called each time a procedure is registered. Use for framework integration, route registration, or logging.
75
+
76
+ ### Return Value
77
+
78
+ | Method | Description |
79
+ |--------|-------------|
80
+ | `Create(name, config, handler)` | Register a standard async procedure |
81
+ | `CreateStream(name, config, handler)` | Register a streaming async generator procedure |
82
+ | `getProcedures()` | Returns array of all registered procedure metadata |
83
+ | `getProcedure(name)` | Returns single procedure by name, or `undefined` |
84
+ | `removeProcedure(name)` | Removes procedure, returns `true` if found |
85
+ | `clear()` | Removes all registered procedures |
86
+
87
+ ### Type Parameters
88
+
89
+ - `TContext` — Base context type injected into every handler. Merged with `TLocalContext` (adds `error()` and optional `signal`).
90
+ - `TExtendedConfig` — Additional config fields required on every procedure's config object (e.g., `RPCConfig` adds `scope` and `version`).
91
+
92
+ ---
93
+
94
+ ## Create\<TName, TParams, TReturnType\>(name, config, handler)
95
+
96
+ Registers a standard async procedure.
97
+
98
+ ```typescript
99
+ function Create<TName extends string, TParams, TReturnType>(
100
+ name: TName,
101
+ config: {
102
+ description?: string
103
+ schema?: {
104
+ params?: TParams // TypeBox schema — validated at runtime
105
+ returnType?: TReturnType // Documentation only — NOT validated
106
+ }
107
+ } & TExtendedConfig,
108
+ handler: (
109
+ ctx: TContext & TLocalContext & { isPrevalidated?: boolean },
110
+ params: TSchemaLib<TParams>
111
+ ) => Promise<TSchemaLib<TReturnType>>
112
+ ): {
113
+ [K in TName]: (ctx: TContext, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
114
+ procedure: (ctx: TContext, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
115
+ info: {
116
+ name: TName
117
+ description?: string
118
+ schema: { params?: TJSONSchema; returnType?: TJSONSchema }
119
+ validation?: { params?: (params: any) => { errors?: TSchemaValidationError[] } }
120
+ } & TExtendedConfig
121
+ }
122
+ ```
123
+
124
+ ### Parameters
125
+
126
+ - `name` — Unique procedure name. Becomes a dynamic property on the return object.
127
+ - `config.description` — Human-readable description.
128
+ - `config.schema.params` — Input schema. Validated at runtime using AJV.
129
+ - `config.schema.returnType` — Return type schema. Documentation only.
130
+ - `handler` — Async function `(ctx, params) => Promise<result>`.
131
+
132
+ ### Handler Context
133
+
134
+ | Property | Type | Description |
135
+ |----------|------|-------------|
136
+ | `...TContext` | varies | All base context fields |
137
+ | `error(message, meta?)` | `Function` | Creates `ProcedureError` — throw this for business logic errors |
138
+ | `signal?` | `AbortSignal` | Present when HTTP implementation injects it (Express/Hono do this automatically) |
139
+ | `isPrevalidated?` | `boolean` | `true` when HTTP impl already validated params — skips AJV validation |
140
+
141
+ ### Return Value
142
+
143
+ - `{ [name]: handler }` — Dynamically named property (e.g., `{ GetUser: fn }`)
144
+ - `{ procedure: handler }` — Same handler under a fixed key
145
+ - `{ info: metadata }` — Registration metadata with computed JSON schemas and validation functions
146
+
147
+ ### Throws
148
+
149
+ - `ProcedureRegistrationError` — If schema extraction/compilation fails, or duplicate name
150
+ - `ProcedureValidationError` — At call time if params fail validation
151
+ - `ProcedureError` — At call time if handler throws (wraps original error in `cause`)
152
+
153
+ ---
154
+
155
+ ## CreateStream\<TName, TParams, TYieldType, TReturnType\>(name, config, handler)
156
+
157
+ Registers a streaming procedure (async generator).
158
+
159
+ ```typescript
160
+ function CreateStream<TName extends string, TParams, TYieldType, TReturnType = void>(
161
+ name: TName,
162
+ config: {
163
+ description?: string
164
+ schema?: {
165
+ params?: TParams // Validated at runtime
166
+ yieldType?: TYieldType // Validated per-yield only if validateYields: true
167
+ returnType?: TReturnType // Documentation only
168
+ }
169
+ validateYields?: boolean // Default: false
170
+ } & TExtendedConfig,
171
+ handler: (
172
+ ctx: TContext & TStreamContext & { isPrevalidated?: boolean },
173
+ params: TSchemaLib<TParams>
174
+ ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
175
+ ): {
176
+ [K in TName]: (ctx: TContext, params: TSchemaLib<TParams>) => AsyncGenerator<...>
177
+ procedure: (ctx: TContext, params: TSchemaLib<TParams>) => AsyncGenerator<...>
178
+ info: {
179
+ name: TName
180
+ isStream: true
181
+ description?: string
182
+ schema: { params?: TJSONSchema; yieldType?: TJSONSchema; returnType?: TJSONSchema }
183
+ validation?: {
184
+ params?: (params: any) => { errors?: TSchemaValidationError[] }
185
+ yield?: (value: any) => { errors?: TSchemaValidationError[] }
186
+ }
187
+ } & TExtendedConfig
188
+ }
189
+ ```
190
+
191
+ ### Handler Context
192
+
193
+ Same as Create, except:
194
+
195
+ - `ctx.signal` — **Always present** (guaranteed `AbortSignal`)
196
+ - On normal stream completion: `signal.reason === 'stream-completed'`
197
+ - On client disconnect: `signal.aborted === true`, `signal.reason` is the abort reason
198
+
199
+ ### Signal Lifecycle
200
+
201
+ ```
202
+ Stream starts → signal.aborted = false
203
+ Client aborts → signal.aborted = true, reason = external reason
204
+ Stream completes → signal.aborted = true, reason = 'stream-completed'
205
+ ```
206
+
207
+ Handlers can distinguish normal completion from client disconnection by checking `signal.reason`.
208
+
209
+ ### Yield Validation
210
+
211
+ When `validateYields: true`:
212
+ - Each yielded value is validated against `schema.yieldType`
213
+ - Failures throw `ProcedureYieldValidationError`
214
+ - Default is `false` (no yield validation)
215
+
216
+ ---
217
+
218
+ ## Error Classes
219
+
220
+ ### ProcedureError
221
+
222
+ Base error class for all procedure errors.
223
+
224
+ ```typescript
225
+ class ProcedureError extends Error {
226
+ readonly procedureName: string
227
+ readonly message: string
228
+ readonly meta?: object
229
+ readonly cause?: unknown
230
+ readonly definedAt?: DefinitionLocation
231
+ readonly definitionStack?: string
232
+
233
+ constructor(procedureName: string, message: string, meta?: object, definitionInfo?: DefinitionInfo)
234
+ getDefinitionLocation(): string | undefined
235
+ }
236
+ ```
237
+
238
+ - Created via `ctx.error(message, meta?)` in handlers
239
+ - Unhandled handler exceptions are wrapped: original error in `cause`
240
+ - Stack trace enhanced with procedure definition location
241
+
242
+ ### ProcedureValidationError extends ProcedureError
243
+
244
+ Thrown when params schema validation fails.
245
+
246
+ ```typescript
247
+ class ProcedureValidationError extends ProcedureError {
248
+ readonly errors?: TSchemaValidationError[]
249
+ constructor(procedureName: string, message: string, errors?: TSchemaValidationError[], definitionInfo?: DefinitionInfo)
250
+ }
251
+ ```
252
+
253
+ - `errors` — Array of AJV error objects with `keyword`, `instancePath`, `message`, `params`
254
+ - Message format: `"Validation error message - field.path validation message, ..."`
255
+
256
+ ### ProcedureYieldValidationError extends ProcedureError
257
+
258
+ Thrown when yield validation fails in CreateStream (when `validateYields: true`).
259
+
260
+ ```typescript
261
+ class ProcedureYieldValidationError extends ProcedureError {
262
+ readonly errors?: TSchemaValidationError[]
263
+ constructor(procedureName: string, message: string, errors?: TSchemaValidationError[], definitionInfo?: DefinitionInfo)
264
+ }
265
+ ```
266
+
267
+ ### ProcedureRegistrationError extends ProcedureError
268
+
269
+ Thrown at registration time (schema extraction/compilation failure, duplicate name).
270
+
271
+ ```typescript
272
+ class ProcedureRegistrationError extends ProcedureError {
273
+ constructor(procedureName: string, message: string, definitionInfo?: DefinitionInfo)
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Schema Utilities
280
+
281
+ ### extractJsonSchema(libSchema)
282
+
283
+ Converts a TypeBox schema to JSON Schema.
284
+
285
+ ```typescript
286
+ function extractJsonSchema(libSchema: unknown): TJSONSchema | undefined
287
+ ```
288
+
289
+ - TypeBox schemas are valid JSON Schema — returned as-is
290
+ - Returns `undefined` if schema type not recognized
291
+
292
+ ### schemaParser(schema, onParseError)
293
+
294
+ Compiles schemas into AJV validator functions.
295
+
296
+ ```typescript
297
+ function schemaParser(
298
+ schema: { params?: unknown; returnType?: unknown; yieldType?: unknown },
299
+ onParseError: (errors: { params?: string; returnType?: string; yieldType?: string }) => void
300
+ ): TSchemaParsed
301
+ ```
302
+
303
+ ### isTypeboxSchema(schema)
304
+
305
+ Type guard for TypeBox schema detection.
306
+
307
+ ```typescript
308
+ function isTypeboxSchema(schema: any): schema is TSchema
309
+ ```
310
+
311
+ ### TSchemaLib\<SchemaLibType\>
312
+
313
+ Maps schema type to inferred TypeScript type:
314
+ - TypeBox `TSchema` → `Static<T>`
315
+ - Otherwise → `unknown`
316
+
317
+ ---
318
+
319
+ ## ExpressRPCAppBuilder
320
+
321
+ HTTP implementation for Express.
322
+
323
+ ```typescript
324
+ class ExpressRPCAppBuilder {
325
+ constructor(config?: {
326
+ app?: express.Express
327
+ pathPrefix?: string
328
+ onRequestStart?: (req: express.Request) => void
329
+ onRequestEnd?: (req: express.Request, res: express.Response) => void
330
+ onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
331
+ onError?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
332
+ })
333
+
334
+ register<TFactory>(
335
+ factory: TFactory,
336
+ factoryContext: Context | ((req: Request) => Context | Promise<Context>),
337
+ extendProcedureDoc?: ({ base, procedure }) => Record<string, any>
338
+ ): this
339
+
340
+ build(): express.Application
341
+
342
+ static makeRPCHttpRoutePath(params: { name: string; config: RPCConfig; prefix?: string }): string
343
+
344
+ get app(): express.Express
345
+ get docs(): RPCHttpRouteDoc[]
346
+ }
347
+ ```
348
+
349
+ ### Route Path
350
+
351
+ `POST {pathPrefix}/{scope}/{kebab-name}/{version}`
352
+
353
+ - `scope` can be string or string array (joined as path segments)
354
+ - Procedure name is converted to kebab-case
355
+
356
+ ### Context Resolution
357
+
358
+ - Static object: `builder.register(factory, { userId: 'system' })`
359
+ - Sync function: `builder.register(factory, (req) => ({ auth: req.headers.authorization }))`
360
+ - Async function: `builder.register(factory, async (req) => ({ user: await getUser(req) }))`
361
+
362
+ ### Signal Injection
363
+
364
+ Express builder creates a lazy AbortController and aborts on request close. The `signal` is injected into handler context automatically.
365
+
366
+ ---
367
+
368
+ ## HonoRPCAppBuilder
369
+
370
+ HTTP implementation for Hono. Same API as ExpressRPCAppBuilder but receives Hono `Context` instead of Express `Request`.
371
+
372
+ ```typescript
373
+ class HonoRPCAppBuilder {
374
+ constructor(config?: {
375
+ app?: Hono
376
+ pathPrefix?: string
377
+ onRequestStart?: (c: Context) => void
378
+ onRequestEnd?: (c: Context) => void
379
+ onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
380
+ onError?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
381
+ })
382
+
383
+ register<TFactory>(
384
+ factory: TFactory,
385
+ factoryContext: Context | ((c: HonoContext) => Context | Promise<Context>),
386
+ extendProcedureDoc?: ({ base, procedure }) => Record<string, any>
387
+ ): this
388
+
389
+ build(): Hono
390
+ get app(): Hono
391
+ get docs(): RPCHttpRouteDoc[]
392
+ }
393
+ ```
394
+
395
+ ### Signal Injection
396
+
397
+ Uses `c.req.raw.signal` — the native Request signal from the Hono context.
398
+
399
+ ---
400
+
401
+ ## HonoStreamAppBuilder\<TErrorData\>
402
+
403
+ Streaming HTTP implementation for Hono. Supports SSE and text streaming modes.
404
+
405
+ ```typescript
406
+ class HonoStreamAppBuilder<TErrorData = unknown> {
407
+ constructor(config?: {
408
+ app?: Hono
409
+ pathPrefix?: string
410
+ defaultStreamMode?: StreamMode // 'sse' | 'text', default 'sse'
411
+ onRequestStart?: (c: Context) => void
412
+ onRequestEnd?: (c: Context) => void
413
+ onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
414
+ onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
415
+ onPreStreamError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
416
+ onMidStreamError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => MidStreamErrorResult<TErrorData> | undefined
417
+ })
418
+
419
+ register<TFactory>(
420
+ factory: TFactory,
421
+ factoryContext: Context | ((c: HonoContext) => Context | Promise<Context>),
422
+ options?: {
423
+ streamMode?: StreamMode
424
+ extendProcedureDoc?: ({ base, procedure }) => Record<string, any>
425
+ }
426
+ ): this
427
+
428
+ build(): Hono
429
+ get app(): Hono
430
+ get docs(): StreamHttpRouteDoc[]
431
+ }
432
+ ```
433
+
434
+ ### Stream Modes
435
+
436
+ **SSE mode** (`'sse'`):
437
+ - Content-Type: `text/event-stream`
438
+ - Each yield → `event: {name}\nid: {counter}\ndata: {json}\n\n`
439
+ - Supports custom event/id/retry via `sse()` helper
440
+
441
+ **Text mode** (`'text'`):
442
+ - Content-Type: `text/plain`
443
+ - Each yield → `{json}\n`
444
+
445
+ ### sse(data, options?)
446
+
447
+ Attaches SSE metadata to a yielded value.
448
+
449
+ ```typescript
450
+ function sse<T extends object>(data: T, options?: {
451
+ event?: string // SSE event type (default: procedure name)
452
+ id?: string // SSE event ID (default: sequential counter)
453
+ retry?: number // Reconnection interval in ms
454
+ }): T
455
+ ```
456
+
457
+ ### HTTP Methods
458
+
459
+ Both GET and POST are registered for each streaming procedure:
460
+ - **GET**: params extracted from query string
461
+ - **POST**: params extracted from JSON body
462
+
463
+ ### MidStreamErrorResult
464
+
465
+ ```typescript
466
+ type MidStreamErrorResult<TErrorData = unknown> = {
467
+ data: TErrorData // Data to yield as final event
468
+ closeStream?: boolean // Close stream after? (default: true)
469
+ }
470
+ ```
471
+
472
+ ### Error Handling
473
+
474
+ - **Pre-stream errors** (validation, context): Returns JSON error response (no streaming started). Custom handling via `onPreStreamError`.
475
+ - **Mid-stream errors** (generator throws): Yields error as final event, closes stream. Custom handling via `onMidStreamError`.
476
+
477
+ ---
478
+
479
+ ## HTTP Types
480
+
481
+ ### RPCConfig
482
+
483
+ ```typescript
484
+ interface RPCConfig {
485
+ scope: string | string[]
486
+ version: number
487
+ }
488
+ ```
489
+
490
+ ### RPCHttpRouteDoc
491
+
492
+ ```typescript
493
+ interface RPCHttpRouteDoc extends RPCConfig {
494
+ name: string
495
+ path: string
496
+ method: 'post'
497
+ jsonSchema: {
498
+ body?: Record<string, unknown>
499
+ response?: Record<string, unknown>
500
+ }
501
+ }
502
+ ```
503
+
504
+ ### StreamHttpRouteDoc
505
+
506
+ ```typescript
507
+ interface StreamHttpRouteDoc extends RPCConfig {
508
+ name: string
509
+ path: string
510
+ methods: ('get' | 'post')[]
511
+ streamMode: StreamMode
512
+ jsonSchema: {
513
+ params?: Record<string, unknown>
514
+ yieldType?: Record<string, unknown>
515
+ returnType?: Record<string, unknown>
516
+ }
517
+ }
518
+ ```
519
+
520
+ ---
521
+
522
+ ## Stack Utilities
523
+
524
+ ### captureDefinitionInfo()
525
+
526
+ Captures stack trace and extracts first user-code frame location. Used internally to enhance error messages.
527
+
528
+ ```typescript
529
+ function captureDefinitionInfo(): DefinitionInfo
530
+ ```
531
+
532
+ ### DefinitionLocation
533
+
534
+ ```typescript
535
+ type DefinitionLocation = {
536
+ file: string
537
+ line: number
538
+ column: number
539
+ raw: string
540
+ }
541
+ ```
542
+
543
+ ### DefinitionInfo
544
+
545
+ ```typescript
546
+ type DefinitionInfo = {
547
+ definedAt?: DefinitionLocation
548
+ definitionStack?: string
549
+ }
550
+ ```