ts-procedures 4.0.1 → 5.0.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.
@@ -3,7 +3,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest'
3
3
  import { Hono } from 'hono'
4
4
  import { v } from 'suretype'
5
5
  import { Procedures } from '../../../index.js'
6
- import { HonoStreamAppBuilder } from './index.js'
6
+ import { HonoStreamAppBuilder, sse } from './index.js'
7
7
  import { RPCConfig } from '../../types.js'
8
8
 
9
9
  /**
@@ -23,7 +23,7 @@ describe('HonoStreamAppBuilder', () => {
23
23
 
24
24
  RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* () {
25
25
  yield { message: 'hello' }
26
- yield { data: { message: 'world' } }
26
+ yield { message: 'world' }
27
27
  })
28
28
 
29
29
  builder.register(RPC, () => ({ userId: '123' }))
@@ -45,7 +45,7 @@ describe('HonoStreamAppBuilder', () => {
45
45
  const RPC = Procedures<{ userId: string }, RPCConfig>()
46
46
 
47
47
  RPC.CreateStream('StreamData', { scope: 'data', version: 1 }, async function* () {
48
- yield { data: { data: 1 } }
48
+ yield { data: 1 }
49
49
  })
50
50
 
51
51
  builder.register(RPC, () => ({ userId: '123' }))
@@ -84,9 +84,9 @@ describe('HonoStreamAppBuilder', () => {
84
84
  const RPC = Procedures<{}, RPCConfig>()
85
85
 
86
86
  RPC.CreateStream('Counter', { scope: 'counter', version: 1 }, async function* () {
87
- yield { data: { count: 1 } }
88
- yield { data: { count: 2 } }
89
- yield { data: { count: 3 } }
87
+ yield { count: 1 }
88
+ yield { count: 2 }
89
+ yield { count: 3 }
90
90
  })
91
91
 
92
92
  builder.register(RPC, () => ({}))
@@ -111,7 +111,7 @@ describe('HonoStreamAppBuilder', () => {
111
111
  const RPC = Procedures<{}, RPCConfig>()
112
112
 
113
113
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
114
- yield { data: { ok: true } }
114
+ yield { ok: true }
115
115
  })
116
116
 
117
117
  builder.register(RPC, () => ({}))
@@ -126,7 +126,7 @@ describe('HonoStreamAppBuilder', () => {
126
126
  const RPC = Procedures<{}, RPCConfig>()
127
127
 
128
128
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
129
- yield { data: { ok: true } }
129
+ yield { ok: true }
130
130
  })
131
131
 
132
132
  builder.register(RPC, () => ({}))
@@ -191,7 +191,7 @@ describe('HonoStreamAppBuilder', () => {
191
191
  const RPC = Procedures<{}, RPCConfig>()
192
192
 
193
193
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
194
- yield { data: { method: 'works' } }
194
+ yield { method: 'works' }
195
195
  })
196
196
 
197
197
  builder.register(RPC, () => ({}))
@@ -206,7 +206,7 @@ describe('HonoStreamAppBuilder', () => {
206
206
  const RPC = Procedures<{}, RPCConfig>()
207
207
 
208
208
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
209
- yield { data: { method: 'works' } }
209
+ yield { method: 'works' }
210
210
  })
211
211
 
212
212
  builder.register(RPC, () => ({}))
@@ -268,7 +268,7 @@ describe('HonoStreamAppBuilder', () => {
268
268
  const RPC = Procedures<{}, RPCConfig>()
269
269
 
270
270
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
271
- yield { data: { ok: true } }
271
+ yield { ok: true }
272
272
  })
273
273
 
274
274
  builder.register(RPC, () => ({}))
@@ -283,7 +283,7 @@ describe('HonoStreamAppBuilder', () => {
283
283
  const RPC = Procedures<{}, RPCConfig>()
284
284
 
285
285
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
286
- yield { data: { ok: true } }
286
+ yield { ok: true }
287
287
  })
288
288
 
289
289
  builder.register(RPC, () => ({}))
@@ -298,7 +298,7 @@ describe('HonoStreamAppBuilder', () => {
298
298
  const RPC = Procedures<{}, RPCConfig>()
299
299
 
300
300
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
301
- yield { data: {} }
301
+ yield {}
302
302
  })
303
303
 
304
304
  builder.register(RPC, () => ({}))
@@ -318,7 +318,7 @@ describe('HonoStreamAppBuilder', () => {
318
318
  const RPC = Procedures<{}, RPCConfig>()
319
319
 
320
320
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
321
- yield { data: { ok: true } }
321
+ yield { ok: true }
322
322
  })
323
323
 
324
324
  builder.register(RPC, () => ({}))
@@ -336,7 +336,7 @@ describe('HonoStreamAppBuilder', () => {
336
336
  const RPC = Procedures<{}, RPCConfig>()
337
337
 
338
338
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
339
- yield { data: { ok: true } }
339
+ yield { ok: true }
340
340
  })
341
341
 
342
342
  builder.register(RPC, () => ({}))
@@ -355,7 +355,7 @@ describe('HonoStreamAppBuilder', () => {
355
355
  const RPC = Procedures<{}, RPCConfig>()
356
356
 
357
357
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
358
- yield { data: { ok: true } }
358
+ yield { ok: true }
359
359
  })
360
360
 
361
361
  builder.register(RPC, () => ({}))
@@ -373,7 +373,7 @@ describe('HonoStreamAppBuilder', () => {
373
373
  const RPC = Procedures<{}, RPCConfig>()
374
374
 
375
375
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
376
- yield { data: { ok: true } }
376
+ yield { ok: true }
377
377
  })
378
378
 
379
379
  builder.register(RPC, () => ({}))
@@ -400,7 +400,7 @@ describe('HonoStreamAppBuilder', () => {
400
400
 
401
401
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
402
402
  order.push('handler')
403
- yield { data: { ok: true } }
403
+ yield { ok: true }
404
404
  })
405
405
 
406
406
  builder.register(RPC, () => ({}))
@@ -448,7 +448,7 @@ describe('HonoStreamAppBuilder', () => {
448
448
  },
449
449
  },
450
450
  async function* (ctx, params) {
451
- yield { data: { count: params.count } }
451
+ yield { count: params.count }
452
452
  }
453
453
  )
454
454
 
@@ -471,7 +471,7 @@ describe('HonoStreamAppBuilder', () => {
471
471
  const RPC = Procedures<{}, RPCConfig>()
472
472
 
473
473
  RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
474
- yield { data: { count: 1 } }
474
+ yield { count: 1 }
475
475
  throw new Error('Stream error')
476
476
  })
477
477
 
@@ -521,7 +521,7 @@ describe('HonoStreamAppBuilder', () => {
521
521
  },
522
522
  },
523
523
  async function* (ctx, params) {
524
- yield { data: { count: params.count } }
524
+ yield { count: params.count }
525
525
  }
526
526
  )
527
527
 
@@ -558,7 +558,7 @@ describe('HonoStreamAppBuilder', () => {
558
558
  },
559
559
  },
560
560
  async function* (ctx, params) {
561
- yield { data: { count: params.count } }
561
+ yield { count: params.count }
562
562
  }
563
563
  )
564
564
 
@@ -584,7 +584,7 @@ describe('HonoStreamAppBuilder', () => {
584
584
  const RPC = Procedures<{ userId: string }, RPCConfig>()
585
585
 
586
586
  RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
587
- yield { data: { userId: ctx.userId } }
587
+ yield { userId: ctx.userId }
588
588
  })
589
589
 
590
590
  builder.register(RPC, () => {
@@ -618,7 +618,7 @@ describe('HonoStreamAppBuilder', () => {
618
618
  const RPC = Procedures<{}, RPCConfig>()
619
619
 
620
620
  RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
621
- yield { data: { type: 'data', value: 1 } }
621
+ yield { type: 'data', value: 1 }
622
622
  throw new Error('Something broke')
623
623
  })
624
624
 
@@ -794,14 +794,15 @@ describe('HonoStreamAppBuilder', () => {
794
794
  expect(doc.jsonSchema.params).toBeDefined()
795
795
  expect(doc.jsonSchema.returnType).toBeDefined()
796
796
 
797
- // SSE envelope fields are merged into yieldType
797
+ // yieldType is nested under SSE envelope's data property
798
798
  const yt = doc.jsonSchema.yieldType as Record<string, any>
799
+ expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.')
799
800
  expect(yt.required).toEqual(['data', 'event', 'id'])
800
801
  expect(yt.properties.event).toEqual({ type: 'string' })
801
802
  expect(yt.properties.id).toEqual({ type: 'string' })
802
803
  expect(yt.properties.retry).toEqual({ type: 'number' })
803
- // Developer's data property is preserved
804
- expect(yt.properties.message).toBeDefined()
804
+ // Developer's yieldType is nested under data
805
+ expect(yt.properties.data.properties.message).toBeDefined()
805
806
  })
806
807
 
807
808
  test('SSE mode generates SSE envelope even when no yieldType is defined', () => {
@@ -818,7 +819,9 @@ describe('HonoStreamAppBuilder', () => {
818
819
  const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
819
820
  expect(yt).toBeDefined()
820
821
  expect(yt.type).toBe('object')
822
+ expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.')
821
823
  expect(yt.required).toEqual(['data', 'event', 'id'])
824
+ // data is empty schema when no yieldType defined
822
825
  expect(yt.properties.data).toEqual({})
823
826
  expect(yt.properties.event).toEqual({ type: 'string' })
824
827
  expect(yt.properties.id).toEqual({ type: 'string' })
@@ -849,25 +852,21 @@ describe('HonoStreamAppBuilder', () => {
849
852
  expect(yt.properties?.retry).toBeUndefined()
850
853
  })
851
854
 
852
- test('developer-defined event/id in yieldType are preserved', () => {
855
+ test('yieldType with id property does not collide with SSE id field', () => {
856
+ // User's yieldType has an `id` field (number) — this should be nested under
857
+ // the SSE envelope's `data` property, not collide with the SSE `id` (string)
853
858
  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'),
859
+ id: v.number(),
860
+ message: v.string(),
857
861
  })
858
862
  const builder = new HonoStreamAppBuilder()
859
863
  const RPC = Procedures<{}, RPCConfig>()
860
864
 
861
865
  RPC.CreateStream(
862
- 'CustomSSE',
866
+ 'Notifications',
863
867
  { scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
864
868
  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
- }
869
+ yield { id: 42, message: 'hello' }
871
870
  }
872
871
  )
873
872
 
@@ -876,11 +875,11 @@ describe('HonoStreamAppBuilder', () => {
876
875
 
877
876
  const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
878
877
  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' })
878
+ // SSE envelope id is a string
879
+ expect(yt.properties.id).toEqual({ type: 'string' })
880
+ // User's id (number) is safely nested under data
881
+ expect(yt.properties.data.properties.id.type).toBe('number')
882
+ expect(yt.properties.data.properties.message.type).toBe('string')
884
883
  })
885
884
 
886
885
  test('streamMode is recorded in docs', () => {
@@ -927,7 +926,7 @@ describe('HonoStreamAppBuilder', () => {
927
926
 
928
927
  // Streaming procedure (should be registered)
929
928
  RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
930
- yield { data: { ok: true } }
929
+ yield { ok: true }
931
930
  })
932
931
 
933
932
  builder.register(RPC, () => ({}))
@@ -956,7 +955,7 @@ describe('HonoStreamAppBuilder', () => {
956
955
  const RPC = Procedures<{}, RPCConfig>()
957
956
 
958
957
  RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
959
- yield { data: {} }
958
+ yield {}
960
959
  })
961
960
 
962
961
  builder.register(RPC, () => ({}), {
@@ -979,7 +978,7 @@ describe('HonoStreamAppBuilder', () => {
979
978
  const RPC = Procedures<{}, RPCConfig>()
980
979
 
981
980
  RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
982
- yield { data: {} }
981
+ yield {}
983
982
  })
984
983
 
985
984
  builder.register(RPC, () => ({}), {
@@ -1050,7 +1049,7 @@ describe('HonoStreamAppBuilder', () => {
1050
1049
  const TextRPC = Procedures<{}, RPCConfig>()
1051
1050
 
1052
1051
  SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
1053
- yield { data: { mode: 'sse' } }
1052
+ yield { mode: 'sse' }
1054
1053
  })
1055
1054
 
1056
1055
  TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
@@ -1114,7 +1113,7 @@ describe('HonoStreamAppBuilder', () => {
1114
1113
 
1115
1114
  RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
1116
1115
  receivedIsPrevalidated = ctx.isPrevalidated
1117
- yield { data: { ok: true } }
1116
+ yield { ok: true }
1118
1117
  })
1119
1118
 
1120
1119
  builder.register(RPC, () => ({}))
@@ -1200,13 +1199,13 @@ describe('HonoStreamAppBuilder', () => {
1200
1199
  // SSE Yield Shape Tests
1201
1200
  // --------------------------------------------------------------------------
1202
1201
  describe('SSE yield shape', () => {
1203
- test('custom event names in yields', async () => {
1202
+ test('custom event names via sse() helper', async () => {
1204
1203
  const builder = new HonoStreamAppBuilder()
1205
1204
  const RPC = Procedures<{}, RPCConfig>()
1206
1205
 
1207
1206
  RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
1208
- yield { data: { type: 'user_joined' }, event: 'join' }
1209
- yield { data: { type: 'message' }, event: 'chat' }
1207
+ yield sse({ type: 'user_joined' }, { event: 'join' })
1208
+ yield sse({ type: 'message' }, { event: 'chat' })
1210
1209
  })
1211
1210
 
1212
1211
  builder.register(RPC, () => ({}))
@@ -1220,13 +1219,13 @@ describe('HonoStreamAppBuilder', () => {
1220
1219
  expect(text).not.toContain('event: Events')
1221
1220
  })
1222
1221
 
1223
- test('custom id in yields', async () => {
1222
+ test('custom id via sse() helper', async () => {
1224
1223
  const builder = new HonoStreamAppBuilder()
1225
1224
  const RPC = Procedures<{}, RPCConfig>()
1226
1225
 
1227
1226
  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' }
1227
+ yield sse({ msg: 'first' }, { id: 'msg-001' })
1228
+ yield sse({ msg: 'second' }, { id: 'msg-002' })
1230
1229
  })
1231
1230
 
1232
1231
  builder.register(RPC, () => ({}))
@@ -1244,8 +1243,8 @@ describe('HonoStreamAppBuilder', () => {
1244
1243
  const RPC = Procedures<{}, RPCConfig>()
1245
1244
 
1246
1245
  RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
1247
- yield { data: 'already a string' }
1248
- yield { data: { needs: 'stringify' } }
1246
+ yield 'already a string'
1247
+ yield { needs: 'stringify' }
1249
1248
  })
1250
1249
 
1251
1250
  builder.register(RPC, () => ({}))
@@ -1265,9 +1264,9 @@ describe('HonoStreamAppBuilder', () => {
1265
1264
  const RPC = Procedures<{}, RPCConfig>()
1266
1265
 
1267
1266
  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 } }
1267
+ yield { value: 1 }
1268
+ yield sse({ value: 2 }, { event: 'custom' })
1269
+ yield { value: 3 }
1271
1270
  })
1272
1271
 
1273
1272
  builder.register(RPC, () => ({}))
@@ -1288,6 +1287,106 @@ describe('HonoStreamAppBuilder', () => {
1288
1287
  })
1289
1288
  })
1290
1289
 
1290
+ // --------------------------------------------------------------------------
1291
+ // sse() Helper Tests
1292
+ // --------------------------------------------------------------------------
1293
+ describe('sse() helper', () => {
1294
+ test('tagged yields with custom event/id/retry', async () => {
1295
+ const builder = new HonoStreamAppBuilder()
1296
+ const RPC = Procedures<{}, RPCConfig>()
1297
+
1298
+ RPC.CreateStream('Tagged', { scope: 'tagged', version: 1 }, async function* () {
1299
+ yield sse({ count: 1 }, { event: 'tick', id: 'evt-1', retry: 5000 })
1300
+ })
1301
+
1302
+ builder.register(RPC, () => ({}))
1303
+ const app = builder.build()
1304
+
1305
+ const res = await app.request('/tagged/tagged/1')
1306
+ const text = await res.text()
1307
+
1308
+ expect(text).toContain('event: tick')
1309
+ expect(text).toContain('id: evt-1')
1310
+ expect(text).toContain('retry: 5000')
1311
+ expect(text).toContain('data: {"count":1}')
1312
+ })
1313
+
1314
+ test('plain domain objects use procedure name and auto-incremented id', async () => {
1315
+ const builder = new HonoStreamAppBuilder()
1316
+ const RPC = Procedures<{}, RPCConfig>()
1317
+
1318
+ RPC.CreateStream('Plain', { scope: 'plain', version: 1 }, async function* () {
1319
+ yield { a: 1 }
1320
+ yield { a: 2 }
1321
+ })
1322
+
1323
+ builder.register(RPC, () => ({}))
1324
+ const app = builder.build()
1325
+
1326
+ const res = await app.request('/plain/plain/1')
1327
+ const text = await res.text()
1328
+
1329
+ const messages = text.split('\n\n').filter(Boolean)
1330
+ expect(messages[0]).toContain('event: Plain')
1331
+ expect(messages[0]).toContain('id: 0')
1332
+ expect(messages[0]).toContain('data: {"a":1}')
1333
+ expect(messages[1]).toContain('event: Plain')
1334
+ expect(messages[1]).toContain('id: 1')
1335
+ expect(messages[1]).toContain('data: {"a":2}')
1336
+ })
1337
+
1338
+ test('sse() metadata is invisible in text mode', async () => {
1339
+ const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
1340
+ const RPC = Procedures<{}, RPCConfig>()
1341
+
1342
+ RPC.CreateStream('TextTagged', { scope: 'text', version: 1 }, async function* () {
1343
+ yield sse({ count: 1 }, { event: 'tick' })
1344
+ yield { count: 2 }
1345
+ })
1346
+
1347
+ builder.register(RPC, () => ({}))
1348
+ const app = builder.build()
1349
+
1350
+ const res = await app.request('/text/text-tagged/1')
1351
+ const text = await res.text()
1352
+ const lines = text.trim().split('\n')
1353
+
1354
+ // Text mode just JSON-stringifies — sse() metadata is not visible
1355
+ expect(JSON.parse(lines[0]!)).toEqual({ count: 1 })
1356
+ expect(JSON.parse(lines[1]!)).toEqual({ count: 2 })
1357
+ })
1358
+
1359
+ test('sse() with partial options', async () => {
1360
+ const builder = new HonoStreamAppBuilder()
1361
+ const RPC = Procedures<{}, RPCConfig>()
1362
+
1363
+ RPC.CreateStream('Partial', { scope: 'partial', version: 1 }, async function* () {
1364
+ yield sse({ v: 1 }, { event: 'custom' })
1365
+ yield sse({ v: 2 }, { id: 'my-id' })
1366
+ yield sse({ v: 3 })
1367
+ })
1368
+
1369
+ builder.register(RPC, () => ({}))
1370
+ const app = builder.build()
1371
+
1372
+ const res = await app.request('/partial/partial/1')
1373
+ const text = await res.text()
1374
+ const messages = text.split('\n\n').filter(Boolean)
1375
+
1376
+ // First: custom event, auto id
1377
+ expect(messages[0]).toContain('event: custom')
1378
+ expect(messages[0]).toContain('id: 0')
1379
+
1380
+ // Second: default event, custom id
1381
+ expect(messages[1]).toContain('event: Partial')
1382
+ expect(messages[1]).toContain('id: my-id')
1383
+
1384
+ // Third: sse() with no options — same as plain object (defaults)
1385
+ expect(messages[2]).toContain('event: Partial')
1386
+ expect(messages[2]).toContain('id: 2')
1387
+ })
1388
+ })
1389
+
1291
1390
  // --------------------------------------------------------------------------
1292
1391
  // Integration Test
1293
1392
  // --------------------------------------------------------------------------
@@ -1330,8 +1429,8 @@ describe('HonoStreamAppBuilder', () => {
1330
1429
  defaultStreamMode: 'text',
1331
1430
  onRequestStart: () => events.push('request-start'),
1332
1431
  onRequestEnd: () => events.push('request-end'),
1333
- onStreamStart: (proc) => events.push(`stream-start:${proc.name}`),
1334
- onStreamEnd: (proc) => events.push(`stream-end:${proc.name}`),
1432
+ onStreamStart: () => events.push('stream-start'),
1433
+ onStreamEnd: () => events.push('stream-end'),
1335
1434
  })
1336
1435
 
1337
1436
  builder.register(RPC, (c) => ({
@@ -1360,8 +1459,8 @@ describe('HonoStreamAppBuilder', () => {
1360
1459
 
1361
1460
  // Verify hooks were called
1362
1461
  expect(events).toContain('request-start')
1363
- expect(events).toContain('stream-start:WatchNotifications')
1364
- expect(events).toContain('stream-end:WatchNotifications')
1462
+ expect(events).toContain('stream-start')
1463
+ expect(events).toContain('stream-end')
1365
1464
  expect(events).toContain('request-end')
1366
1465
  })
1367
1466
  })
@@ -9,16 +9,29 @@ 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)
34
+ * @property data - The data to write as the SSE `data:` field content (should match yieldType schema)
22
35
  * @property event - Optional SSE event name (defaults to procedure name if data provided, 'error' otherwise)
23
36
  * @property id - Optional SSE event id (auto-incremented if not provided)
24
37
  * @property closeStream - Whether to close the stream after writing (defaults to true)
@@ -96,7 +109,6 @@ export class HonoStreamAppBuilder {
96
109
  await next()
97
110
  })
98
111
  }
99
-
100
112
  }
101
113
 
102
114
  /**
@@ -242,11 +254,20 @@ export class HonoStreamAppBuilder {
242
254
  try {
243
255
  for await (const value of generator) {
244
256
  const currentId = eventId++
257
+ const meta = getSSEMeta(value)
258
+
259
+ const data =
260
+ typeof value === 'string'
261
+ ? value
262
+ : value != null
263
+ ? JSON.stringify(value)
264
+ : ''
265
+
245
266
  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 }),
267
+ data,
268
+ event: meta?.event ?? procedure.name,
269
+ id: meta?.id ?? String(currentId),
270
+ ...(meta?.retry !== undefined && { retry: meta.retry }),
250
271
  })
251
272
  }
252
273
  } catch (error) {
@@ -370,34 +391,16 @@ export class HonoStreamAppBuilder {
370
391
  jsonSchema.params = config.schema.params
371
392
  }
372
393
  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
- }
394
+ jsonSchema.yieldType = {
395
+ type: 'object',
396
+ description: 'SSE message envelope. The data field contains the procedure yield value.',
397
+ required: ['data', 'event', 'id'],
398
+ properties: {
399
+ data: config.schema?.yieldType ?? {},
400
+ event: { type: 'string' },
401
+ id: { type: 'string' },
402
+ retry: { type: 'number' },
403
+ },
401
404
  }
402
405
  } else if (config.schema?.yieldType) {
403
406
  // Text mode: pass through as-is