ts-procedures 3.4.0 → 4.0.1

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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/explicit-function-return-type */
1
2
  import { describe, expect, test, vi, beforeEach } from 'vitest'
2
3
  import { Hono } from 'hono'
3
4
  import { v } from 'suretype'
@@ -20,9 +21,9 @@ describe('HonoStreamAppBuilder', () => {
20
21
  const builder = new HonoStreamAppBuilder()
21
22
  const RPC = Procedures<{ userId: string }, RPCConfig>()
22
23
 
23
- RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* (ctx) {
24
+ RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* () {
24
25
  yield { message: 'hello' }
25
- yield { message: 'world' }
26
+ yield { data: { message: 'world' } }
26
27
  })
27
28
 
28
29
  builder.register(RPC, () => ({ userId: '123' }))
@@ -44,7 +45,7 @@ describe('HonoStreamAppBuilder', () => {
44
45
  const RPC = Procedures<{ userId: string }, RPCConfig>()
45
46
 
46
47
  RPC.CreateStream('StreamData', { scope: 'data', version: 1 }, async function* () {
47
- yield { data: 1 }
48
+ yield { data: { data: 1 } }
48
49
  })
49
50
 
50
51
  builder.register(RPC, () => ({ userId: '123' }))
@@ -83,9 +84,9 @@ describe('HonoStreamAppBuilder', () => {
83
84
  const RPC = Procedures<{}, RPCConfig>()
84
85
 
85
86
  RPC.CreateStream('Counter', { scope: 'counter', version: 1 }, async function* () {
86
- yield { count: 1 }
87
- yield { count: 2 }
88
- yield { count: 3 }
87
+ yield { data: { count: 1 } }
88
+ yield { data: { count: 2 } }
89
+ yield { data: { count: 3 } }
89
90
  })
90
91
 
91
92
  builder.register(RPC, () => ({}))
@@ -110,7 +111,7 @@ describe('HonoStreamAppBuilder', () => {
110
111
  const RPC = Procedures<{}, RPCConfig>()
111
112
 
112
113
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
113
- yield { ok: true }
114
+ yield { data: { ok: true } }
114
115
  })
115
116
 
116
117
  builder.register(RPC, () => ({}))
@@ -125,7 +126,7 @@ describe('HonoStreamAppBuilder', () => {
125
126
  const RPC = Procedures<{}, RPCConfig>()
126
127
 
127
128
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
128
- yield { ok: true }
129
+ yield { data: { ok: true } }
129
130
  })
130
131
 
131
132
  builder.register(RPC, () => ({}))
@@ -190,7 +191,7 @@ describe('HonoStreamAppBuilder', () => {
190
191
  const RPC = Procedures<{}, RPCConfig>()
191
192
 
192
193
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
193
- yield { method: 'works' }
194
+ yield { data: { method: 'works' } }
194
195
  })
195
196
 
196
197
  builder.register(RPC, () => ({}))
@@ -205,7 +206,7 @@ describe('HonoStreamAppBuilder', () => {
205
206
  const RPC = Procedures<{}, RPCConfig>()
206
207
 
207
208
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
208
- yield { method: 'works' }
209
+ yield { data: { method: 'works' } }
209
210
  })
210
211
 
211
212
  builder.register(RPC, () => ({}))
@@ -267,7 +268,7 @@ describe('HonoStreamAppBuilder', () => {
267
268
  const RPC = Procedures<{}, RPCConfig>()
268
269
 
269
270
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
270
- yield { ok: true }
271
+ yield { data: { ok: true } }
271
272
  })
272
273
 
273
274
  builder.register(RPC, () => ({}))
@@ -282,7 +283,7 @@ describe('HonoStreamAppBuilder', () => {
282
283
  const RPC = Procedures<{}, RPCConfig>()
283
284
 
284
285
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
285
- yield { ok: true }
286
+ yield { data: { ok: true } }
286
287
  })
287
288
 
288
289
  builder.register(RPC, () => ({}))
@@ -297,7 +298,7 @@ describe('HonoStreamAppBuilder', () => {
297
298
  const RPC = Procedures<{}, RPCConfig>()
298
299
 
299
300
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
300
- yield {}
301
+ yield { data: {} }
301
302
  })
302
303
 
303
304
  builder.register(RPC, () => ({}))
@@ -317,7 +318,7 @@ describe('HonoStreamAppBuilder', () => {
317
318
  const RPC = Procedures<{}, RPCConfig>()
318
319
 
319
320
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
320
- yield { ok: true }
321
+ yield { data: { ok: true } }
321
322
  })
322
323
 
323
324
  builder.register(RPC, () => ({}))
@@ -335,7 +336,7 @@ describe('HonoStreamAppBuilder', () => {
335
336
  const RPC = Procedures<{}, RPCConfig>()
336
337
 
337
338
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
338
- yield { ok: true }
339
+ yield { data: { ok: true } }
339
340
  })
340
341
 
341
342
  builder.register(RPC, () => ({}))
@@ -354,7 +355,7 @@ describe('HonoStreamAppBuilder', () => {
354
355
  const RPC = Procedures<{}, RPCConfig>()
355
356
 
356
357
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
357
- yield { ok: true }
358
+ yield { data: { ok: true } }
358
359
  })
359
360
 
360
361
  builder.register(RPC, () => ({}))
@@ -372,7 +373,7 @@ describe('HonoStreamAppBuilder', () => {
372
373
  const RPC = Procedures<{}, RPCConfig>()
373
374
 
374
375
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
375
- yield { ok: true }
376
+ yield { data: { ok: true } }
376
377
  })
377
378
 
378
379
  builder.register(RPC, () => ({}))
@@ -399,7 +400,7 @@ describe('HonoStreamAppBuilder', () => {
399
400
 
400
401
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
401
402
  order.push('handler')
402
- yield { ok: true }
403
+ yield { data: { ok: true } }
403
404
  })
404
405
 
405
406
  builder.register(RPC, () => ({}))
@@ -447,7 +448,7 @@ describe('HonoStreamAppBuilder', () => {
447
448
  },
448
449
  },
449
450
  async function* (ctx, params) {
450
- yield { count: params.count }
451
+ yield { data: { count: params.count } }
451
452
  }
452
453
  )
453
454
 
@@ -470,7 +471,7 @@ describe('HonoStreamAppBuilder', () => {
470
471
  const RPC = Procedures<{}, RPCConfig>()
471
472
 
472
473
  RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
473
- yield { count: 1 }
474
+ yield { data: { count: 1 } }
474
475
  throw new Error('Stream error')
475
476
  })
476
477
 
@@ -520,7 +521,7 @@ describe('HonoStreamAppBuilder', () => {
520
521
  },
521
522
  },
522
523
  async function* (ctx, params) {
523
- yield { count: params.count }
524
+ yield { data: { count: params.count } }
524
525
  }
525
526
  )
526
527
 
@@ -557,7 +558,7 @@ describe('HonoStreamAppBuilder', () => {
557
558
  },
558
559
  },
559
560
  async function* (ctx, params) {
560
- yield { count: params.count }
561
+ yield { data: { count: params.count } }
561
562
  }
562
563
  )
563
564
 
@@ -583,7 +584,7 @@ describe('HonoStreamAppBuilder', () => {
583
584
  const RPC = Procedures<{ userId: string }, RPCConfig>()
584
585
 
585
586
  RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
586
- yield { userId: ctx.userId }
587
+ yield { data: { userId: ctx.userId } }
587
588
  })
588
589
 
589
590
  builder.register(RPC, () => {
@@ -603,7 +604,7 @@ describe('HonoStreamAppBuilder', () => {
603
604
  test('onMidStreamError returns custom value written to SSE stream', async () => {
604
605
  const onMidStreamError = vi.fn((procedure, c, error) => {
605
606
  return {
606
- value: {
607
+ data: {
607
608
  type: 'error',
608
609
  code: 'STREAM_FAILED',
609
610
  message: error.message,
@@ -617,7 +618,7 @@ describe('HonoStreamAppBuilder', () => {
617
618
  const RPC = Procedures<{}, RPCConfig>()
618
619
 
619
620
  RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
620
- yield { type: 'data', value: 1 }
621
+ yield { data: { type: 'data', value: 1 } }
621
622
  throw new Error('Something broke')
622
623
  })
623
624
 
@@ -641,7 +642,7 @@ describe('HonoStreamAppBuilder', () => {
641
642
  test('onMidStreamError returns custom value written to text stream', async () => {
642
643
  const onMidStreamError = vi.fn((procedure, c, error) => {
643
644
  return {
644
- value: { type: 'error', message: error.message },
645
+ data: { type: 'error', message: error.message },
645
646
  }
646
647
  })
647
648
 
@@ -791,8 +792,95 @@ describe('HonoStreamAppBuilder', () => {
791
792
  expect(doc.methods).toEqual(['get', 'post'])
792
793
  expect(doc.streamMode).toBe('sse')
793
794
  expect(doc.jsonSchema.params).toBeDefined()
794
- expect(doc.jsonSchema.yieldType).toBeDefined()
795
795
  expect(doc.jsonSchema.returnType).toBeDefined()
796
+
797
+ // SSE envelope fields are merged into yieldType
798
+ const yt = doc.jsonSchema.yieldType as Record<string, any>
799
+ expect(yt.required).toEqual(['data', 'event', 'id'])
800
+ expect(yt.properties.event).toEqual({ type: 'string' })
801
+ expect(yt.properties.id).toEqual({ type: 'string' })
802
+ expect(yt.properties.retry).toEqual({ type: 'number' })
803
+ // Developer's data property is preserved
804
+ expect(yt.properties.message).toBeDefined()
805
+ })
806
+
807
+ test('SSE mode generates SSE envelope even when no yieldType is defined', () => {
808
+ const builder = new HonoStreamAppBuilder()
809
+ const RPC = Procedures<{}, RPCConfig>()
810
+
811
+ RPC.CreateStream('NoYield', { scope: 'test', version: 1 }, async function* () {
812
+ yield {}
813
+ })
814
+
815
+ builder.register(RPC, () => ({}))
816
+ builder.build()
817
+
818
+ const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
819
+ expect(yt).toBeDefined()
820
+ expect(yt.type).toBe('object')
821
+ expect(yt.required).toEqual(['data', 'event', 'id'])
822
+ expect(yt.properties.data).toEqual({})
823
+ expect(yt.properties.event).toEqual({ type: 'string' })
824
+ expect(yt.properties.id).toEqual({ type: 'string' })
825
+ expect(yt.properties.retry).toEqual({ type: 'number' })
826
+ })
827
+
828
+ test('text mode passes yieldType through as-is', () => {
829
+ const yieldSchema = v.object({ chunk: v.string() })
830
+ const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
831
+ const RPC = Procedures<{}, RPCConfig>()
832
+
833
+ RPC.CreateStream(
834
+ 'TextStream',
835
+ { scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
836
+ async function* () {
837
+ yield { chunk: 'hi' }
838
+ }
839
+ )
840
+
841
+ builder.register(RPC, () => ({}))
842
+ builder.build()
843
+
844
+ const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
845
+ expect(yt).toBeDefined()
846
+ // Text mode should NOT have SSE envelope fields injected
847
+ expect(yt.properties?.event).toBeUndefined()
848
+ expect(yt.properties?.id).toBeUndefined()
849
+ expect(yt.properties?.retry).toBeUndefined()
850
+ })
851
+
852
+ test('developer-defined event/id in yieldType are preserved', () => {
853
+ const yieldSchema = v.object({
854
+ data: v.object({ msg: v.string() }),
855
+ event: v.string().const('custom-event'),
856
+ id: v.string().const('fixed-id'),
857
+ })
858
+ const builder = new HonoStreamAppBuilder()
859
+ const RPC = Procedures<{}, RPCConfig>()
860
+
861
+ RPC.CreateStream(
862
+ 'CustomSSE',
863
+ { scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
864
+ async function* () {
865
+ // suretype sucks, we have to assert this as the exact type to satisfy the const assertions in the schema
866
+ yield { data: { msg: 'hi' }, event: 'custom-event', id: 'fixed-id' } as {
867
+ data: { msg: string }
868
+ event: 'custom-event'
869
+ id: 'fixed-id'
870
+ }
871
+ }
872
+ )
873
+
874
+ builder.register(RPC, () => ({}))
875
+ builder.build()
876
+
877
+ const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
878
+ expect(yt.required).toEqual(['data', 'event', 'id'])
879
+ // Developer's custom event/id definitions take precedence
880
+ expect(yt.properties.event.const).toBe('custom-event')
881
+ expect(yt.properties.id.const).toBe('fixed-id')
882
+ // retry is still added as a base default
883
+ expect(yt.properties.retry).toEqual({ type: 'number' })
796
884
  })
797
885
 
798
886
  test('streamMode is recorded in docs', () => {
@@ -839,7 +927,7 @@ describe('HonoStreamAppBuilder', () => {
839
927
 
840
928
  // Streaming procedure (should be registered)
841
929
  RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
842
- yield { ok: true }
930
+ yield { data: { ok: true } }
843
931
  })
844
932
 
845
933
  builder.register(RPC, () => ({}))
@@ -868,11 +956,11 @@ describe('HonoStreamAppBuilder', () => {
868
956
  const RPC = Procedures<{}, RPCConfig>()
869
957
 
870
958
  RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
871
- yield {}
959
+ yield { data: {} }
872
960
  })
873
961
 
874
962
  builder.register(RPC, () => ({}), {
875
- extendProcedureDoc: ({ base, procedure }) => ({
963
+ extendProcedureDoc: ({ procedure }) => ({
876
964
  summary: `Stream events endpoint`,
877
965
  tags: ['events'],
878
966
  operationId: procedure.name,
@@ -891,7 +979,7 @@ describe('HonoStreamAppBuilder', () => {
891
979
  const RPC = Procedures<{}, RPCConfig>()
892
980
 
893
981
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
894
- yield {}
982
+ yield { data: {} }
895
983
  })
896
984
 
897
985
  builder.register(RPC, () => ({}), {
@@ -962,7 +1050,7 @@ describe('HonoStreamAppBuilder', () => {
962
1050
  const TextRPC = Procedures<{}, RPCConfig>()
963
1051
 
964
1052
  SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
965
- yield { mode: 'sse' }
1053
+ yield { data: { mode: 'sse' } }
966
1054
  })
967
1055
 
968
1056
  TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
@@ -1026,7 +1114,7 @@ describe('HonoStreamAppBuilder', () => {
1026
1114
 
1027
1115
  RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
1028
1116
  receivedIsPrevalidated = ctx.isPrevalidated
1029
- yield { ok: true }
1117
+ yield { data: { ok: true } }
1030
1118
  })
1031
1119
 
1032
1120
  builder.register(RPC, () => ({}))
@@ -1108,6 +1196,98 @@ describe('HonoStreamAppBuilder', () => {
1108
1196
  })
1109
1197
  })
1110
1198
 
1199
+ // --------------------------------------------------------------------------
1200
+ // SSE Yield Shape Tests
1201
+ // --------------------------------------------------------------------------
1202
+ describe('SSE yield shape', () => {
1203
+ test('custom event names in yields', async () => {
1204
+ const builder = new HonoStreamAppBuilder()
1205
+ const RPC = Procedures<{}, RPCConfig>()
1206
+
1207
+ RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
1208
+ yield { data: { type: 'user_joined' }, event: 'join' }
1209
+ yield { data: { type: 'message' }, event: 'chat' }
1210
+ })
1211
+
1212
+ builder.register(RPC, () => ({}))
1213
+ const app = builder.build()
1214
+
1215
+ const res = await app.request('/events/events/1')
1216
+ const text = await res.text()
1217
+
1218
+ expect(text).toContain('event: join')
1219
+ expect(text).toContain('event: chat')
1220
+ expect(text).not.toContain('event: Events')
1221
+ })
1222
+
1223
+ test('custom id in yields', async () => {
1224
+ const builder = new HonoStreamAppBuilder()
1225
+ const RPC = Procedures<{}, RPCConfig>()
1226
+
1227
+ RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
1228
+ yield { data: { msg: 'first' }, id: 'msg-001' }
1229
+ yield { data: { msg: 'second' }, id: 'msg-002' }
1230
+ })
1231
+
1232
+ builder.register(RPC, () => ({}))
1233
+ const app = builder.build()
1234
+
1235
+ const res = await app.request('/events/events/1')
1236
+ const text = await res.text()
1237
+
1238
+ expect(text).toContain('id: msg-001')
1239
+ expect(text).toContain('id: msg-002')
1240
+ })
1241
+
1242
+ test('string data pass-through without double-stringify', async () => {
1243
+ const builder = new HonoStreamAppBuilder()
1244
+ const RPC = Procedures<{}, RPCConfig>()
1245
+
1246
+ RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
1247
+ yield { data: 'already a string' }
1248
+ yield { data: { needs: 'stringify' } }
1249
+ })
1250
+
1251
+ builder.register(RPC, () => ({}))
1252
+ const app = builder.build()
1253
+
1254
+ const res = await app.request('/events/events/1')
1255
+ const text = await res.text()
1256
+
1257
+ // String data should be passed through as-is (not JSON-stringified again)
1258
+ expect(text).toContain('data: already a string')
1259
+ // Object data should be JSON-stringified
1260
+ expect(text).toContain('data: {"needs":"stringify"}')
1261
+ })
1262
+
1263
+ test('default event falls back to procedure name when omitted', async () => {
1264
+ const builder = new HonoStreamAppBuilder()
1265
+ const RPC = Procedures<{}, RPCConfig>()
1266
+
1267
+ RPC.CreateStream('MyProcedure', { scope: 'test', version: 1 }, async function* () {
1268
+ yield { data: { value: 1 } }
1269
+ yield { data: { value: 2 }, event: 'custom' }
1270
+ yield { data: { value: 3 } }
1271
+ })
1272
+
1273
+ builder.register(RPC, () => ({}))
1274
+ const app = builder.build()
1275
+
1276
+ const res = await app.request('/test/my-procedure/1')
1277
+ const text = await res.text()
1278
+
1279
+ // Split into individual SSE messages
1280
+ const messages = text.split('\n\n').filter(Boolean)
1281
+
1282
+ // First and third should use procedure name as event
1283
+ expect(messages[0]).toContain('event: MyProcedure')
1284
+ // Second should use custom event
1285
+ expect(messages[1]).toContain('event: custom')
1286
+ // Third should fall back to procedure name
1287
+ expect(messages[2]).toContain('event: MyProcedure')
1288
+ })
1289
+ })
1290
+
1111
1291
  // --------------------------------------------------------------------------
1112
1292
  // Integration Test
1113
1293
  // --------------------------------------------------------------------------
@@ -9,13 +9,24 @@ import { ProcedureValidationError } from '../../../errors.js'
9
9
 
10
10
  export type { StreamHttpRouteDoc, StreamMode }
11
11
 
12
+ export type SSEYield = {
13
+ data: string | unknown
14
+ event?: string
15
+ id?: string
16
+ retry?: number
17
+ }
18
+
12
19
  /**
13
20
  * Result from onMidStreamError callback.
14
- * @property value - The value to write to the stream (should match yieldType schema)
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)
15
24
  * @property closeStream - Whether to close the stream after writing (defaults to true)
16
25
  */
17
26
  export type MidStreamErrorResult = {
18
- value: unknown
27
+ data: unknown
28
+ event?: string
29
+ id?: string
19
30
  closeStream?: boolean
20
31
  }
21
32
 
@@ -48,7 +59,7 @@ export type HonoStreamAppBuilderConfig = {
48
59
  * Should return a value matching your yieldType schema (e.g., error variant of a union).
49
60
  * Return undefined to use default behavior (writes { error: message }).
50
61
  *
51
- * @returns { value, closeStream? } - value to yield, whether to close after (default true)
62
+ * @returns { data, event?, id?, closeStream? } - data to yield, optional SSE fields, whether to close after (default true)
52
63
  */
53
64
  onMidStreamError?: (
54
65
  procedure: TStreamProcedureRegistration,
@@ -230,10 +241,12 @@ export class HonoStreamAppBuilder {
230
241
  let eventId = 0
231
242
  try {
232
243
  for await (const value of generator) {
244
+ const currentId = eventId++
233
245
  await stream.writeSSE({
234
- data: JSON.stringify(value),
235
- event: procedure.name,
236
- id: String(eventId++),
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 }),
237
250
  })
238
251
  }
239
252
  } catch (error) {
@@ -245,12 +258,11 @@ export class HonoStreamAppBuilder {
245
258
  }
246
259
 
247
260
  // Write error value to stream
248
- const errorValue = errorResult?.value ?? { error: (error as Error).message }
261
+ const errorData = errorResult?.data ?? { error: (error as Error).message }
249
262
  await stream.writeSSE({
250
- data: JSON.stringify(errorValue),
251
- // Use procedure event name if custom value provided, otherwise 'error'
252
- event: errorResult?.value !== undefined ? procedure.name : 'error',
253
- id: String(eventId++),
263
+ data: typeof errorData === 'string' ? errorData : JSON.stringify(errorData),
264
+ event: errorResult?.event ?? (errorResult?.data !== undefined ? procedure.name : 'error'),
265
+ id: errorResult?.id ?? String(eventId++),
254
266
  })
255
267
 
256
268
  // closeStream defaults to true if not specified
@@ -296,8 +308,8 @@ export class HonoStreamAppBuilder {
296
308
  }
297
309
 
298
310
  // Write error value to stream
299
- const errorValue = errorResult?.value ?? { error: (error as Error).message }
300
- await stream.writeln(JSON.stringify(errorValue))
311
+ const errorData = errorResult?.data ?? { error: (error as Error).message }
312
+ await stream.writeln(JSON.stringify(errorData))
301
313
  } finally {
302
314
  if (this.config?.onStreamEnd) {
303
315
  this.config.onStreamEnd(procedure, c)
@@ -357,7 +369,38 @@ export class HonoStreamAppBuilder {
357
369
  if (config.schema?.params) {
358
370
  jsonSchema.params = config.schema.params
359
371
  }
360
- if (config.schema?.yieldType) {
372
+ 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
+ }
401
+ }
402
+ } else if (config.schema?.yieldType) {
403
+ // Text mode: pass through as-is
361
404
  jsonSchema.yieldType = config.schema.yieldType
362
405
  }
363
406
  if (config.schema?.returnType) {