ts-procedures 4.0.1 → 5.1.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.
@@ -9,28 +9,37 @@ import { ProcedureValidationError } from '../../../errors.js'
9
9
 
10
10
  export type { StreamHttpRouteDoc, StreamMode }
11
11
 
12
- export type SSEYield = {
13
- data: string | unknown
12
+ export type SSEOptions = {
14
13
  event?: string
15
14
  id?: string
16
15
  retry?: number
17
16
  }
18
17
 
18
+ const sseMetadata = new WeakMap<object, SSEOptions>()
19
+
20
+ export function sse<T extends object>(data: T, options?: SSEOptions): T {
21
+ sseMetadata.set(data, options ?? {})
22
+ return data
23
+ }
24
+
25
+ function getSSEMeta(value: unknown): SSEOptions | undefined {
26
+ if (typeof value === 'object' && value !== null) {
27
+ return sseMetadata.get(value)
28
+ }
29
+ return undefined
30
+ }
31
+
19
32
  /**
20
33
  * Result from onMidStreamError callback.
21
- * @property data - The data to write to the stream (should match yieldType schema)
22
- * @property event - Optional SSE event name (defaults to procedure name if data provided, 'error' otherwise)
23
- * @property id - Optional SSE event id (auto-incremented if not provided)
34
+ * @property data - The data to write as the SSE `data:` field content (should match yieldType schema)
24
35
  * @property closeStream - Whether to close the stream after writing (defaults to true)
25
36
  */
26
- export type MidStreamErrorResult = {
27
- data: unknown
28
- event?: string
29
- id?: string
37
+ export type MidStreamErrorResult<TErrorData = unknown> = {
38
+ data: TErrorData
30
39
  closeStream?: boolean
31
40
  }
32
41
 
33
- export type HonoStreamAppBuilderConfig = {
42
+ export type HonoStreamAppBuilderConfig<TErrorData = unknown> = {
34
43
  /**
35
44
  * An existing Hono application instance to use.
36
45
  * If not provided, a new instance will be created.
@@ -42,8 +51,8 @@ export type HonoStreamAppBuilderConfig = {
42
51
  defaultStreamMode?: StreamMode
43
52
  onRequestStart?: (c: Context) => void
44
53
  onRequestEnd?: (c: Context) => void
45
- onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context) => void
46
- onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context) => void
54
+ onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
55
+ onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
47
56
  /**
48
57
  * Called for errors BEFORE streaming starts (validation, auth, context resolution).
49
58
  * Return value IS used as the HTTP response.
@@ -51,7 +60,7 @@ export type HonoStreamAppBuilderConfig = {
51
60
  onPreStreamError?: (
52
61
  procedure: TStreamProcedureRegistration,
53
62
  c: Context,
54
- error: Error
63
+ error: ProcedureValidationError | Error
55
64
  ) => Response | Promise<Response>
56
65
  /**
57
66
  * Called for errors DURING streaming (generator throws).
@@ -59,13 +68,15 @@ export type HonoStreamAppBuilderConfig = {
59
68
  * Should return a value matching your yieldType schema (e.g., error variant of a union).
60
69
  * Return undefined to use default behavior (writes { error: message }).
61
70
  *
62
- * @returns { data, event?, id?, closeStream? } - data to yield, optional SSE fields, whether to close after (default true)
71
+ * Use sse() to attach SSE metadata (event, id, retry) to the error data object.
72
+ *
73
+ * @returns { data, closeStream? } - data to yield, whether to close after (default true)
63
74
  */
64
75
  onMidStreamError?: (
65
76
  procedure: TStreamProcedureRegistration,
66
77
  c: Context,
67
78
  error: Error
68
- ) => MidStreamErrorResult | undefined
79
+ ) => MidStreamErrorResult<TErrorData> | undefined
69
80
  }
70
81
 
71
82
  /**
@@ -81,11 +92,11 @@ export type HonoStreamAppBuilderConfig = {
81
92
  * const app = streamApp.app; // Hono application
82
93
  * const docs = streamApp.docs; // Stream route documentation
83
94
  */
84
- export class HonoStreamAppBuilder {
95
+ export class HonoStreamAppBuilder<TErrorData = unknown> {
85
96
  /**
86
97
  * Constructor for HonoStreamAppBuilder.
87
98
  */
88
- constructor(readonly config?: HonoStreamAppBuilderConfig) {
99
+ constructor(readonly config?: HonoStreamAppBuilderConfig<TErrorData>) {
89
100
  if (config?.app) {
90
101
  this._app = config.app
91
102
  }
@@ -96,7 +107,6 @@ export class HonoStreamAppBuilder {
96
107
  await next()
97
108
  })
98
109
  }
99
-
100
110
  }
101
111
 
102
112
  /**
@@ -203,7 +213,7 @@ export class HonoStreamAppBuilder {
203
213
  }
204
214
 
205
215
  if (this.config?.onStreamStart) {
206
- this.config.onStreamStart(procedure, c)
216
+ this.config.onStreamStart(procedure, c, streamMode)
207
217
  }
208
218
 
209
219
  if (streamMode === 'sse') {
@@ -242,16 +252,25 @@ export class HonoStreamAppBuilder {
242
252
  try {
243
253
  for await (const value of generator) {
244
254
  const currentId = eventId++
255
+ const meta = getSSEMeta(value)
256
+
257
+ const data =
258
+ typeof value === 'string'
259
+ ? value
260
+ : value != null
261
+ ? JSON.stringify(value)
262
+ : ''
263
+
245
264
  await stream.writeSSE({
246
- data: typeof value.data === 'string' ? value.data : JSON.stringify(value.data),
247
- event: value.event ?? procedure.name,
248
- id: value.id ?? String(currentId),
249
- ...(value.retry !== undefined && { retry: value.retry }),
265
+ data,
266
+ event: meta?.event ?? procedure.name,
267
+ id: meta?.id ?? String(currentId),
268
+ ...(meta?.retry !== undefined && { retry: meta.retry }),
250
269
  })
251
270
  }
252
271
  } catch (error) {
253
272
  // Get error yield value from callback (onMidStreamError)
254
- let errorResult: MidStreamErrorResult | undefined
273
+ let errorResult: MidStreamErrorResult<TErrorData> | undefined
255
274
 
256
275
  if (this.config?.onMidStreamError) {
257
276
  errorResult = this.config.onMidStreamError(procedure, c, error as Error)
@@ -259,17 +278,20 @@ export class HonoStreamAppBuilder {
259
278
 
260
279
  // Write error value to stream
261
280
  const errorData = errorResult?.data ?? { error: (error as Error).message }
281
+ const sseMeta = getSSEMeta(errorData)
282
+
262
283
  await stream.writeSSE({
263
284
  data: typeof errorData === 'string' ? errorData : JSON.stringify(errorData),
264
- event: errorResult?.event ?? (errorResult?.data !== undefined ? procedure.name : 'error'),
265
- id: errorResult?.id ?? String(eventId++),
285
+ event: sseMeta?.event ?? (errorResult?.data !== undefined ? procedure.name : 'error'),
286
+ id: sseMeta?.id ?? String(eventId++),
287
+ ...(sseMeta?.retry !== undefined && { retry: sseMeta.retry }),
266
288
  })
267
289
 
268
290
  // closeStream defaults to true if not specified
269
291
  // (stream closes naturally after this handler completes)
270
292
  } finally {
271
293
  if (this.config?.onStreamEnd) {
272
- this.config.onStreamEnd(procedure, c)
294
+ this.config.onStreamEnd(procedure, c, 'sse')
273
295
  }
274
296
  if (this.config?.onRequestEnd) {
275
297
  this.config.onRequestEnd(c)
@@ -301,7 +323,7 @@ export class HonoStreamAppBuilder {
301
323
  }
302
324
  } catch (error) {
303
325
  // Get error yield value from callback (onMidStreamError)
304
- let errorResult: MidStreamErrorResult | undefined
326
+ let errorResult: MidStreamErrorResult<TErrorData> | undefined
305
327
 
306
328
  if (this.config?.onMidStreamError) {
307
329
  errorResult = this.config.onMidStreamError(procedure, c, error as Error)
@@ -312,7 +334,7 @@ export class HonoStreamAppBuilder {
312
334
  await stream.writeln(JSON.stringify(errorData))
313
335
  } finally {
314
336
  if (this.config?.onStreamEnd) {
315
- this.config.onStreamEnd(procedure, c)
337
+ this.config.onStreamEnd(procedure, c, 'text')
316
338
  }
317
339
  if (this.config?.onRequestEnd) {
318
340
  this.config.onRequestEnd(c)
@@ -364,40 +386,22 @@ export class HonoStreamAppBuilder {
364
386
  prefix: this.config?.pathPrefix,
365
387
  })
366
388
  const methods = ['get', 'post'] as const
367
- const jsonSchema: { params?: object; yieldType?: object; returnType?: object } = {}
389
+ const jsonSchema: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> } = {}
368
390
 
369
391
  if (config.schema?.params) {
370
392
  jsonSchema.params = config.schema.params
371
393
  }
372
394
  if (streamMode === 'sse') {
373
- const sseBaseProperties = {
374
- event: { type: 'string' },
375
- id: { type: 'string' },
376
- retry: { type: 'number' },
377
- }
378
-
379
- if (config.schema?.yieldType) {
380
- // Developer's yieldType describes the full SSEYield envelope.
381
- // Merge in SSE base fields for any the developer didn't define.
382
- const userSchema = config.schema.yieldType as Record<string, any>
383
- jsonSchema.yieldType = {
384
- ...userSchema,
385
- required: ['data', 'event', 'id'],
386
- properties: {
387
- ...sseBaseProperties,
388
- ...(userSchema.properties ?? {}),
389
- },
390
- }
391
- } else {
392
- // No yieldType defined — generate a complete SSE envelope schema
393
- jsonSchema.yieldType = {
394
- type: 'object',
395
- required: ['data', 'event', 'id'],
396
- properties: {
397
- data: {},
398
- ...sseBaseProperties,
399
- },
400
- }
395
+ jsonSchema.yieldType = {
396
+ type: 'object',
397
+ description: 'SSE message envelope. The data field contains the procedure yield value.',
398
+ required: ['data', 'event', 'id'],
399
+ properties: {
400
+ data: config.schema?.yieldType ?? {},
401
+ event: { type: 'string' },
402
+ id: { type: 'string' },
403
+ retry: { type: 'number' },
404
+ },
401
405
  }
402
406
  } else if (config.schema?.yieldType) {
403
407
  // Text mode: pass through as-is
@@ -10,9 +10,9 @@ export interface StreamHttpRouteDoc extends RPCConfig {
10
10
  methods: ('get' | 'post')[]
11
11
  streamMode: StreamMode
12
12
  jsonSchema: {
13
- params?: object
14
- yieldType?: object
15
- returnType?: object
13
+ params?: Record<string, unknown>
14
+ yieldType?: Record<string, unknown>
15
+ returnType?: Record<string, unknown>
16
16
  }
17
17
  }
18
18
 
@@ -16,8 +16,8 @@ export interface RPCHttpRouteDoc extends RPCConfig {
16
16
  path: string
17
17
  method: 'post'
18
18
  jsonSchema: {
19
- body?: object
20
- response?: object
19
+ body?: Record<string, unknown>
20
+ response?: Record<string, unknown>
21
21
  }
22
22
  }
23
23
 
@@ -29,9 +29,9 @@ export interface StreamHttpRouteDoc extends RPCConfig {
29
29
  methods: ('get' | 'post')[]
30
30
  streamMode: StreamMode
31
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)
32
+ params?: Record<string, unknown> // Query params (GET) or body (POST)
33
+ yieldType?: Record<string, unknown> // Schema for each streamed value
34
+ returnType?: Record<string, unknown> // Final return (optional)
35
35
  }
36
36
  }
37
37