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.
- package/README.md +222 -2
- package/build/errors.d.ts +19 -3
- package/build/errors.js +54 -5
- package/build/errors.js.map +1 -1
- package/build/errors.test.js +82 -0
- package/build/errors.test.js.map +1 -1
- package/build/exports.d.ts +1 -0
- package/build/exports.js +1 -0
- package/build/exports.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +92 -0
- package/build/implementations/http/hono-stream/index.js +229 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +681 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +24 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/types.d.ts +15 -1
- package/build/index.d.ts +62 -3
- package/build/index.js +111 -6
- package/build/index.js.map +1 -1
- package/build/index.test.js +385 -2
- package/build/index.test.js.map +1 -1
- package/build/schema/compute-schema.d.ts +9 -2
- package/build/schema/compute-schema.js +9 -3
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -0
- package/build/schema/parser.js +42 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/types.d.ts +1 -0
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/package.json +1 -1
- package/src/errors.test.ts +110 -0
- package/src/errors.ts +65 -3
- package/src/exports.ts +1 -0
- package/src/implementations/http/README.md +87 -55
- package/src/implementations/http/hono-stream/README.md +261 -0
- package/src/implementations/http/hono-stream/index.test.ts +1009 -0
- package/src/implementations/http/hono-stream/index.ts +327 -0
- package/src/implementations/http/hono-stream/types.ts +29 -0
- package/src/implementations/types.ts +17 -1
- package/src/index.test.ts +525 -41
- package/src/index.ts +210 -8
- package/src/schema/compute-schema.ts +15 -3
- package/src/schema/parser.ts +55 -4
- package/src/schema/types.ts +4 -0
- package/src/stack-utils.test.ts +94 -0
- 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
|
}
|