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.
- package/build/implementations/http/hono-stream/index.d.ts +13 -3
- package/build/implementations/http/hono-stream/index.js +44 -11
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +167 -33
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/implementations/http/hono-stream/README.md +62 -14
- package/src/implementations/http/hono-stream/index.test.ts +213 -33
- package/src/implementations/http/hono-stream/index.ts +57 -14
|
@@ -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* (
|
|
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
|
-
|
|
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
|
-
|
|
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: ({
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
261
|
+
const errorData = errorResult?.data ?? { error: (error as Error).message }
|
|
249
262
|
await stream.writeSSE({
|
|
250
|
-
data: JSON.stringify(
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
300
|
-
await stream.writeln(JSON.stringify(
|
|
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 (
|
|
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) {
|