ts-procedures 3.1.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 (54) hide show
  1. package/README.md +222 -2
  2. package/build/errors.d.ts +19 -3
  3. package/build/errors.js +54 -5
  4. package/build/errors.js.map +1 -1
  5. package/build/errors.test.js +82 -0
  6. package/build/errors.test.js.map +1 -1
  7. package/build/exports.d.ts +1 -0
  8. package/build/exports.js +1 -0
  9. package/build/exports.js.map +1 -1
  10. package/build/implementations/http/hono-stream/index.d.ts +92 -0
  11. package/build/implementations/http/hono-stream/index.js +229 -0
  12. package/build/implementations/http/hono-stream/index.js.map +1 -0
  13. package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
  14. package/build/implementations/http/hono-stream/index.test.js +681 -0
  15. package/build/implementations/http/hono-stream/index.test.js.map +1 -0
  16. package/build/implementations/http/hono-stream/types.d.ts +24 -0
  17. package/build/implementations/http/hono-stream/types.js +2 -0
  18. package/build/implementations/http/hono-stream/types.js.map +1 -0
  19. package/build/implementations/types.d.ts +15 -1
  20. package/build/index.d.ts +62 -3
  21. package/build/index.js +111 -6
  22. package/build/index.js.map +1 -1
  23. package/build/index.test.js +385 -2
  24. package/build/index.test.js.map +1 -1
  25. package/build/schema/compute-schema.d.ts +9 -2
  26. package/build/schema/compute-schema.js +9 -3
  27. package/build/schema/compute-schema.js.map +1 -1
  28. package/build/schema/parser.d.ts +6 -0
  29. package/build/schema/parser.js +42 -0
  30. package/build/schema/parser.js.map +1 -1
  31. package/build/schema/types.d.ts +1 -0
  32. package/build/stack-utils.d.ts +25 -0
  33. package/build/stack-utils.js +95 -0
  34. package/build/stack-utils.js.map +1 -0
  35. package/build/stack-utils.test.d.ts +1 -0
  36. package/build/stack-utils.test.js +80 -0
  37. package/build/stack-utils.test.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/errors.test.ts +110 -0
  40. package/src/errors.ts +65 -3
  41. package/src/exports.ts +1 -0
  42. package/src/implementations/http/README.md +87 -55
  43. package/src/implementations/http/hono-stream/README.md +261 -0
  44. package/src/implementations/http/hono-stream/index.test.ts +1009 -0
  45. package/src/implementations/http/hono-stream/index.ts +327 -0
  46. package/src/implementations/http/hono-stream/types.ts +29 -0
  47. package/src/implementations/types.ts +17 -1
  48. package/src/index.test.ts +525 -41
  49. package/src/index.ts +210 -8
  50. package/src/schema/compute-schema.ts +15 -3
  51. package/src/schema/parser.ts +55 -4
  52. package/src/schema/types.ts +4 -0
  53. package/src/stack-utils.test.ts +94 -0
  54. package/src/stack-utils.ts +129 -0
@@ -0,0 +1,327 @@
1
+ import { Hono, Context } from 'hono'
2
+ import { streamSSE, streamText } from 'hono/streaming'
3
+ import { kebabCase } from 'es-toolkit/string'
4
+ import { castArray } from 'es-toolkit/compat'
5
+ import { TStreamProcedureRegistration } from '../../../index.js'
6
+ import {
7
+ ExtractConfig,
8
+ ExtractContext,
9
+ ProceduresFactory,
10
+ RPCConfig,
11
+ } from '../../types.js'
12
+ import { HonoStreamFactoryItem, StreamHttpRouteDoc, StreamMode } from './types.js'
13
+
14
+ export type { StreamHttpRouteDoc, StreamMode }
15
+
16
+ export type HonoStreamAppBuilderConfig = {
17
+ /**
18
+ * An existing Hono application instance to use.
19
+ * If not provided, a new instance will be created.
20
+ */
21
+ app?: Hono
22
+ /** Optional path prefix for all stream routes. */
23
+ pathPrefix?: string
24
+ /** Default stream mode for all routes. Defaults to 'sse'. */
25
+ defaultStreamMode?: StreamMode
26
+ onRequestStart?: (c: Context) => void
27
+ onRequestEnd?: (c: Context) => void
28
+ onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context) => void
29
+ onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context) => void
30
+ /**
31
+ * Error handler called when a streaming procedure throws an error.
32
+ */
33
+ onStreamError?: (
34
+ procedure: TStreamProcedureRegistration,
35
+ c: Context,
36
+ error: Error
37
+ ) => Response | Promise<Response>
38
+ }
39
+
40
+ /**
41
+ * Builder class for creating a Hono application with streaming RPC routes.
42
+ *
43
+ * Usage:
44
+ * const StreamRPC = Procedures<StreamContext, RPCConfig>()
45
+ *
46
+ * const streamApp = new HonoStreamAppBuilder()
47
+ * .register(StreamRPC, (c): Promise<StreamContext> => { /* context resolution logic * / })
48
+ * .build();
49
+ *
50
+ * const app = streamApp.app; // Hono application
51
+ * const docs = streamApp.docs; // Stream route documentation
52
+ */
53
+ export class HonoStreamAppBuilder {
54
+ /**
55
+ * Constructor for HonoStreamAppBuilder.
56
+ */
57
+ constructor(readonly config?: HonoStreamAppBuilderConfig) {
58
+ if (config?.app) {
59
+ this._app = config.app
60
+ }
61
+
62
+ if (config?.onRequestStart) {
63
+ this._app.use('*', async (c, next) => {
64
+ config.onRequestStart!(c)
65
+ await next()
66
+ })
67
+ }
68
+
69
+ if (config?.onRequestEnd) {
70
+ this._app.use('*', async (c, next) => {
71
+ await next()
72
+ config.onRequestEnd!(c)
73
+ })
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Generates the stream route path based on the RPC configuration.
79
+ */
80
+ static makeStreamHttpRoutePath({
81
+ name,
82
+ config,
83
+ prefix,
84
+ }: {
85
+ name: string
86
+ prefix?: string
87
+ config: RPCConfig
88
+ }) {
89
+ const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
90
+
91
+ return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
92
+ }
93
+
94
+ /**
95
+ * Instance method wrapper for makeStreamHttpRoutePath that uses the builder's pathPrefix.
96
+ */
97
+ makeStreamHttpRoutePath(name: string, config: RPCConfig): string {
98
+ return HonoStreamAppBuilder.makeStreamHttpRoutePath({
99
+ name,
100
+ config,
101
+ prefix: this.config?.pathPrefix,
102
+ })
103
+ }
104
+
105
+ private factories: HonoStreamFactoryItem<any>[] = []
106
+
107
+ private _app: Hono = new Hono()
108
+ private _docs: (StreamHttpRouteDoc & object)[] = []
109
+
110
+ get app(): Hono {
111
+ return this._app
112
+ }
113
+
114
+ get docs(): StreamHttpRouteDoc[] {
115
+ return this._docs
116
+ }
117
+
118
+ /**
119
+ * Registers a procedure factory with its context.
120
+ * Only streaming procedures (created with CreateStream) will be registered.
121
+ */
122
+ register<TFactory extends ProceduresFactory>(
123
+ factory: TFactory,
124
+ factoryContext:
125
+ | ExtractContext<TFactory>
126
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
127
+ options?: {
128
+ streamMode?: StreamMode
129
+ extendProcedureDoc?: (params: {
130
+ base: StreamHttpRouteDoc
131
+ procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
132
+ }) => Record<string, any>
133
+ }
134
+ ): this {
135
+ this.factories.push({
136
+ factory,
137
+ factoryContext,
138
+ streamMode: options?.streamMode,
139
+ extendProcedureDoc: options?.extendProcedureDoc,
140
+ } as HonoStreamFactoryItem<any>)
141
+ return this
142
+ }
143
+
144
+ /**
145
+ * Creates a route handler for streaming procedures.
146
+ */
147
+ private createStreamHandler(
148
+ procedure: TStreamProcedureRegistration,
149
+ factoryContext: HonoStreamFactoryItem['factoryContext'],
150
+ streamMode: StreamMode
151
+ ) {
152
+ return async (c: Context) => {
153
+ try {
154
+ const context =
155
+ typeof factoryContext === 'function'
156
+ ? await factoryContext(c)
157
+ : factoryContext
158
+
159
+ // GET: query params, POST: JSON body
160
+ const params =
161
+ c.req.method === 'GET'
162
+ ? Object.fromEntries(new URL(c.req.url).searchParams)
163
+ : await c.req.json().catch(() => ({}))
164
+
165
+ if (this.config?.onStreamStart) {
166
+ this.config.onStreamStart(procedure, c)
167
+ }
168
+
169
+ if (streamMode === 'sse') {
170
+ return this.handleSSEStream(procedure, context, params, c)
171
+ } else {
172
+ return this.handleTextStream(procedure, context, params, c)
173
+ }
174
+ } catch (error) {
175
+ if (this.config?.onStreamError) {
176
+ return this.config.onStreamError(procedure, c, error as Error)
177
+ }
178
+ return c.json({ error: (error as Error).message }, 500)
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handles SSE streaming mode.
185
+ */
186
+ private handleSSEStream(
187
+ procedure: TStreamProcedureRegistration,
188
+ context: any,
189
+ params: any,
190
+ c: Context
191
+ ) {
192
+ return streamSSE(c, async (stream) => {
193
+ const generator = procedure.handler(context, params)
194
+
195
+ stream.onAbort(async () => {
196
+ await generator.return(undefined)
197
+ })
198
+
199
+ try {
200
+ let eventId = 0
201
+ for await (const value of generator) {
202
+ await stream.writeSSE({
203
+ data: JSON.stringify(value),
204
+ event: procedure.name,
205
+ id: String(eventId++),
206
+ })
207
+ }
208
+ } catch (error) {
209
+ // Write error as SSE event before closing
210
+ await stream.writeSSE({
211
+ data: JSON.stringify({ error: (error as Error).message }),
212
+ event: 'error',
213
+ })
214
+ } finally {
215
+ if (this.config?.onStreamEnd) {
216
+ this.config.onStreamEnd(procedure, c)
217
+ }
218
+ }
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Handles text streaming mode.
224
+ */
225
+ private handleTextStream(
226
+ procedure: TStreamProcedureRegistration,
227
+ context: any,
228
+ params: any,
229
+ c: Context
230
+ ) {
231
+ return streamText(c, async (stream) => {
232
+ const generator = procedure.handler(context, params)
233
+
234
+ stream.onAbort(async () => {
235
+ await generator.return(undefined)
236
+ })
237
+
238
+ try {
239
+ for await (const value of generator) {
240
+ await stream.writeln(JSON.stringify(value))
241
+ }
242
+ } catch (error) {
243
+ // Write error as JSON line before closing
244
+ await stream.writeln(JSON.stringify({ error: (error as Error).message }))
245
+ } finally {
246
+ if (this.config?.onStreamEnd) {
247
+ this.config.onStreamEnd(procedure, c)
248
+ }
249
+ }
250
+ })
251
+ }
252
+
253
+ /**
254
+ * Builds and returns the Hono application with registered streaming routes.
255
+ */
256
+ build(): Hono {
257
+ this.factories.forEach(({ factory, factoryContext, streamMode, extendProcedureDoc }) => {
258
+ const mode = streamMode ?? this.config?.defaultStreamMode ?? 'sse'
259
+
260
+ factory
261
+ .getProcedures()
262
+ .filter((p: { isStream?: boolean }): p is TStreamProcedureRegistration => p.isStream === true)
263
+ .forEach((procedure: TStreamProcedureRegistration<any, RPCConfig>) => {
264
+ const route = this.buildStreamHttpRouteDoc(procedure, mode, extendProcedureDoc)
265
+
266
+ this._docs.push(route)
267
+
268
+ const handler = this.createStreamHandler(procedure, factoryContext, mode)
269
+
270
+ // Register both GET and POST handlers
271
+ this._app.get(route.path, handler)
272
+ this._app.post(route.path, handler)
273
+ })
274
+ })
275
+
276
+ return this._app
277
+ }
278
+
279
+ /**
280
+ * Generates the Stream HTTP route documentation for the given procedure.
281
+ */
282
+ private buildStreamHttpRouteDoc(
283
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
284
+ streamMode: StreamMode,
285
+ extendProcedureDoc?: HonoStreamFactoryItem['extendProcedureDoc']
286
+ ): StreamHttpRouteDoc {
287
+ const { config } = procedure
288
+ const path = HonoStreamAppBuilder.makeStreamHttpRoutePath({
289
+ name: procedure.name,
290
+ config,
291
+ prefix: this.config?.pathPrefix,
292
+ })
293
+ const methods = ['get', 'post'] as const
294
+ const jsonSchema: { params?: object; yieldType?: object; returnType?: object } = {}
295
+
296
+ if (config.schema?.params) {
297
+ jsonSchema.params = config.schema.params
298
+ }
299
+ if (config.schema?.yieldType) {
300
+ jsonSchema.yieldType = config.schema.yieldType
301
+ }
302
+ if (config.schema?.returnType) {
303
+ jsonSchema.returnType = config.schema.returnType
304
+ }
305
+
306
+ const base: StreamHttpRouteDoc = {
307
+ name: procedure.name,
308
+ version: config.version,
309
+ scope: config.scope,
310
+ path,
311
+ methods: [...methods],
312
+ streamMode,
313
+ jsonSchema,
314
+ }
315
+
316
+ let extendedDoc: object = {}
317
+
318
+ if (extendProcedureDoc) {
319
+ extendedDoc = extendProcedureDoc({ base, procedure })
320
+ }
321
+
322
+ return {
323
+ ...extendedDoc,
324
+ ...base,
325
+ }
326
+ }
327
+ }
@@ -0,0 +1,29 @@
1
+ import { Context } from 'hono'
2
+ import { TStreamProcedureRegistration } from '../../../index.js'
3
+ import { ExtractConfig, ExtractContext, RPCConfig } from '../../types.js'
4
+
5
+ export type StreamMode = 'sse' | 'text'
6
+
7
+ export interface StreamHttpRouteDoc extends RPCConfig {
8
+ name: string
9
+ path: string
10
+ methods: ('get' | 'post')[]
11
+ streamMode: StreamMode
12
+ jsonSchema: {
13
+ params?: object
14
+ yieldType?: object
15
+ returnType?: object
16
+ }
17
+ }
18
+
19
+ export type HonoStreamFactoryItem<TFactory = any> = {
20
+ factory: TFactory
21
+ factoryContext:
22
+ | ExtractContext<TFactory>
23
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
24
+ streamMode?: StreamMode
25
+ extendProcedureDoc?: (params: {
26
+ base: StreamHttpRouteDoc
27
+ procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
28
+ }) => Record<string, any>
29
+ }
@@ -21,6 +21,20 @@ export interface RPCHttpRouteDoc extends RPCConfig {
21
21
  }
22
22
  }
23
23
 
24
+ export type StreamMode = 'sse' | 'text'
25
+
26
+ export interface StreamHttpRouteDoc extends RPCConfig {
27
+ name: string // procedure name
28
+ path: string
29
+ methods: ('get' | 'post')[]
30
+ streamMode: StreamMode
31
+ jsonSchema: {
32
+ params?: object // Query params (GET) or body (POST)
33
+ yieldType?: object // Schema for each streamed value
34
+ returnType?: object // Final return (optional)
35
+ }
36
+ }
37
+
24
38
  // ================
25
39
  // Utility types
26
40
  // ================
@@ -52,8 +66,10 @@ export type ExtractConfig<TFactory> = TFactory extends {
52
66
  export type ProceduresFactory = {
53
67
  getProcedures: () => Array<{
54
68
  name: string
69
+ isStream?: boolean
55
70
  config: any
56
- handler: (ctx: any, params?: any) => Promise<any>
71
+ handler: (ctx: any, params?: any) => Promise<any> | AsyncGenerator<any, any, unknown>
57
72
  }>
58
73
  Create: (...args: any[]) => any
74
+ CreateStream?: (...args: any[]) => any
59
75
  }