ts-procedures 4.0.0 → 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.
- package/build/implementations/http/hono-stream/index.d.ts +3 -3
- package/build/implementations/http/hono-stream/index.js +36 -5
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +191 -47
- 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 +58 -31
- package/src/implementations/http/hono-stream/index.test.ts +234 -47
- package/src/implementations/http/hono-stream/index.ts +43 -9
|
@@ -1,8 +1,9 @@
|
|
|
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'
|
|
4
5
|
import { Procedures } from '../../../index.js'
|
|
5
|
-
import { HonoStreamAppBuilder } from './index.js'
|
|
6
|
+
import { HonoStreamAppBuilder, sse } from './index.js'
|
|
6
7
|
import { RPCConfig } from '../../types.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -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 {
|
|
26
|
+
yield { 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:
|
|
48
|
+
yield { 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 {
|
|
87
|
-
yield {
|
|
88
|
-
yield {
|
|
87
|
+
yield { count: 1 }
|
|
88
|
+
yield { count: 2 }
|
|
89
|
+
yield { 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 {
|
|
114
|
+
yield { 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 {
|
|
129
|
+
yield { 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 {
|
|
194
|
+
yield { 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 {
|
|
209
|
+
yield { 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 {
|
|
271
|
+
yield { 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 {
|
|
286
|
+
yield { 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 {}
|
|
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 {
|
|
321
|
+
yield { 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 {
|
|
339
|
+
yield { 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 {
|
|
358
|
+
yield { 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 {
|
|
376
|
+
yield { 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 {
|
|
403
|
+
yield { 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 {
|
|
451
|
+
yield { 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 {
|
|
474
|
+
yield { 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 {
|
|
524
|
+
yield { 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 {
|
|
561
|
+
yield { 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 {
|
|
587
|
+
yield { userId: ctx.userId }
|
|
587
588
|
})
|
|
588
589
|
|
|
589
590
|
builder.register(RPC, () => {
|
|
@@ -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 {
|
|
621
|
+
yield { type: 'data', value: 1 }
|
|
621
622
|
throw new Error('Something broke')
|
|
622
623
|
})
|
|
623
624
|
|
|
@@ -791,8 +792,94 @@ 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
|
+
// yieldType is nested under SSE envelope's data property
|
|
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.')
|
|
800
|
+
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
801
|
+
expect(yt.properties.event).toEqual({ type: 'string' })
|
|
802
|
+
expect(yt.properties.id).toEqual({ type: 'string' })
|
|
803
|
+
expect(yt.properties.retry).toEqual({ type: 'number' })
|
|
804
|
+
// Developer's yieldType is nested under data
|
|
805
|
+
expect(yt.properties.data.properties.message).toBeDefined()
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
test('SSE mode generates SSE envelope even when no yieldType is defined', () => {
|
|
809
|
+
const builder = new HonoStreamAppBuilder()
|
|
810
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
811
|
+
|
|
812
|
+
RPC.CreateStream('NoYield', { scope: 'test', version: 1 }, async function* () {
|
|
813
|
+
yield {}
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
builder.register(RPC, () => ({}))
|
|
817
|
+
builder.build()
|
|
818
|
+
|
|
819
|
+
const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
|
|
820
|
+
expect(yt).toBeDefined()
|
|
821
|
+
expect(yt.type).toBe('object')
|
|
822
|
+
expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.')
|
|
823
|
+
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
824
|
+
// data is empty schema when no yieldType defined
|
|
825
|
+
expect(yt.properties.data).toEqual({})
|
|
826
|
+
expect(yt.properties.event).toEqual({ type: 'string' })
|
|
827
|
+
expect(yt.properties.id).toEqual({ type: 'string' })
|
|
828
|
+
expect(yt.properties.retry).toEqual({ type: 'number' })
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
test('text mode passes yieldType through as-is', () => {
|
|
832
|
+
const yieldSchema = v.object({ chunk: v.string() })
|
|
833
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
|
|
834
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
835
|
+
|
|
836
|
+
RPC.CreateStream(
|
|
837
|
+
'TextStream',
|
|
838
|
+
{ scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
|
|
839
|
+
async function* () {
|
|
840
|
+
yield { chunk: 'hi' }
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
builder.register(RPC, () => ({}))
|
|
845
|
+
builder.build()
|
|
846
|
+
|
|
847
|
+
const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
|
|
848
|
+
expect(yt).toBeDefined()
|
|
849
|
+
// Text mode should NOT have SSE envelope fields injected
|
|
850
|
+
expect(yt.properties?.event).toBeUndefined()
|
|
851
|
+
expect(yt.properties?.id).toBeUndefined()
|
|
852
|
+
expect(yt.properties?.retry).toBeUndefined()
|
|
853
|
+
})
|
|
854
|
+
|
|
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)
|
|
858
|
+
const yieldSchema = v.object({
|
|
859
|
+
id: v.number(),
|
|
860
|
+
message: v.string(),
|
|
861
|
+
})
|
|
862
|
+
const builder = new HonoStreamAppBuilder()
|
|
863
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
864
|
+
|
|
865
|
+
RPC.CreateStream(
|
|
866
|
+
'Notifications',
|
|
867
|
+
{ scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
|
|
868
|
+
async function* () {
|
|
869
|
+
yield { id: 42, message: 'hello' }
|
|
870
|
+
}
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
builder.register(RPC, () => ({}))
|
|
874
|
+
builder.build()
|
|
875
|
+
|
|
876
|
+
const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
|
|
877
|
+
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
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')
|
|
796
883
|
})
|
|
797
884
|
|
|
798
885
|
test('streamMode is recorded in docs', () => {
|
|
@@ -839,7 +926,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
839
926
|
|
|
840
927
|
// Streaming procedure (should be registered)
|
|
841
928
|
RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
|
|
842
|
-
yield {
|
|
929
|
+
yield { ok: true }
|
|
843
930
|
})
|
|
844
931
|
|
|
845
932
|
builder.register(RPC, () => ({}))
|
|
@@ -868,11 +955,11 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
868
955
|
const RPC = Procedures<{}, RPCConfig>()
|
|
869
956
|
|
|
870
957
|
RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
|
|
871
|
-
yield {
|
|
958
|
+
yield {}
|
|
872
959
|
})
|
|
873
960
|
|
|
874
961
|
builder.register(RPC, () => ({}), {
|
|
875
|
-
extendProcedureDoc: ({
|
|
962
|
+
extendProcedureDoc: ({ procedure }) => ({
|
|
876
963
|
summary: `Stream events endpoint`,
|
|
877
964
|
tags: ['events'],
|
|
878
965
|
operationId: procedure.name,
|
|
@@ -891,7 +978,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
891
978
|
const RPC = Procedures<{}, RPCConfig>()
|
|
892
979
|
|
|
893
980
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
894
|
-
yield {
|
|
981
|
+
yield {}
|
|
895
982
|
})
|
|
896
983
|
|
|
897
984
|
builder.register(RPC, () => ({}), {
|
|
@@ -962,7 +1049,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
962
1049
|
const TextRPC = Procedures<{}, RPCConfig>()
|
|
963
1050
|
|
|
964
1051
|
SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
|
|
965
|
-
yield {
|
|
1052
|
+
yield { mode: 'sse' }
|
|
966
1053
|
})
|
|
967
1054
|
|
|
968
1055
|
TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
|
|
@@ -1026,7 +1113,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1026
1113
|
|
|
1027
1114
|
RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
|
|
1028
1115
|
receivedIsPrevalidated = ctx.isPrevalidated
|
|
1029
|
-
yield {
|
|
1116
|
+
yield { ok: true }
|
|
1030
1117
|
})
|
|
1031
1118
|
|
|
1032
1119
|
builder.register(RPC, () => ({}))
|
|
@@ -1112,13 +1199,13 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1112
1199
|
// SSE Yield Shape Tests
|
|
1113
1200
|
// --------------------------------------------------------------------------
|
|
1114
1201
|
describe('SSE yield shape', () => {
|
|
1115
|
-
test('custom event names
|
|
1202
|
+
test('custom event names via sse() helper', async () => {
|
|
1116
1203
|
const builder = new HonoStreamAppBuilder()
|
|
1117
1204
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1118
1205
|
|
|
1119
1206
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1120
|
-
yield {
|
|
1121
|
-
yield {
|
|
1207
|
+
yield sse({ type: 'user_joined' }, { event: 'join' })
|
|
1208
|
+
yield sse({ type: 'message' }, { event: 'chat' })
|
|
1122
1209
|
})
|
|
1123
1210
|
|
|
1124
1211
|
builder.register(RPC, () => ({}))
|
|
@@ -1132,13 +1219,13 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1132
1219
|
expect(text).not.toContain('event: Events')
|
|
1133
1220
|
})
|
|
1134
1221
|
|
|
1135
|
-
test('custom id
|
|
1222
|
+
test('custom id via sse() helper', async () => {
|
|
1136
1223
|
const builder = new HonoStreamAppBuilder()
|
|
1137
1224
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1138
1225
|
|
|
1139
1226
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1140
|
-
yield {
|
|
1141
|
-
yield {
|
|
1227
|
+
yield sse({ msg: 'first' }, { id: 'msg-001' })
|
|
1228
|
+
yield sse({ msg: 'second' }, { id: 'msg-002' })
|
|
1142
1229
|
})
|
|
1143
1230
|
|
|
1144
1231
|
builder.register(RPC, () => ({}))
|
|
@@ -1156,8 +1243,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1156
1243
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1157
1244
|
|
|
1158
1245
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1159
|
-
yield
|
|
1160
|
-
yield {
|
|
1246
|
+
yield 'already a string'
|
|
1247
|
+
yield { needs: 'stringify' }
|
|
1161
1248
|
})
|
|
1162
1249
|
|
|
1163
1250
|
builder.register(RPC, () => ({}))
|
|
@@ -1177,9 +1264,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1177
1264
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1178
1265
|
|
|
1179
1266
|
RPC.CreateStream('MyProcedure', { scope: 'test', version: 1 }, async function* () {
|
|
1180
|
-
yield {
|
|
1181
|
-
yield {
|
|
1182
|
-
yield {
|
|
1267
|
+
yield { value: 1 }
|
|
1268
|
+
yield sse({ value: 2 }, { event: 'custom' })
|
|
1269
|
+
yield { value: 3 }
|
|
1183
1270
|
})
|
|
1184
1271
|
|
|
1185
1272
|
builder.register(RPC, () => ({}))
|
|
@@ -1200,6 +1287,106 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1200
1287
|
})
|
|
1201
1288
|
})
|
|
1202
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
|
+
|
|
1203
1390
|
// --------------------------------------------------------------------------
|
|
1204
1391
|
// Integration Test
|
|
1205
1392
|
// --------------------------------------------------------------------------
|
|
@@ -1242,8 +1429,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1242
1429
|
defaultStreamMode: 'text',
|
|
1243
1430
|
onRequestStart: () => events.push('request-start'),
|
|
1244
1431
|
onRequestEnd: () => events.push('request-end'),
|
|
1245
|
-
onStreamStart: (
|
|
1246
|
-
onStreamEnd: (
|
|
1432
|
+
onStreamStart: () => events.push('stream-start'),
|
|
1433
|
+
onStreamEnd: () => events.push('stream-end'),
|
|
1247
1434
|
})
|
|
1248
1435
|
|
|
1249
1436
|
builder.register(RPC, (c) => ({
|
|
@@ -1272,8 +1459,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1272
1459
|
|
|
1273
1460
|
// Verify hooks were called
|
|
1274
1461
|
expect(events).toContain('request-start')
|
|
1275
|
-
expect(events).toContain('stream-start
|
|
1276
|
-
expect(events).toContain('stream-end
|
|
1462
|
+
expect(events).toContain('stream-start')
|
|
1463
|
+
expect(events).toContain('stream-end')
|
|
1277
1464
|
expect(events).toContain('request-end')
|
|
1278
1465
|
})
|
|
1279
1466
|
})
|
|
@@ -9,16 +9,29 @@ import { ProcedureValidationError } from '../../../errors.js'
|
|
|
9
9
|
|
|
10
10
|
export type { StreamHttpRouteDoc, StreamMode }
|
|
11
11
|
|
|
12
|
-
export type
|
|
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
|
|
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
|
|
247
|
-
event:
|
|
248
|
-
id:
|
|
249
|
-
...(
|
|
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) {
|
|
@@ -369,7 +390,20 @@ export class HonoStreamAppBuilder {
|
|
|
369
390
|
if (config.schema?.params) {
|
|
370
391
|
jsonSchema.params = config.schema.params
|
|
371
392
|
}
|
|
372
|
-
if (
|
|
393
|
+
if (streamMode === 'sse') {
|
|
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
|
+
},
|
|
404
|
+
}
|
|
405
|
+
} else if (config.schema?.yieldType) {
|
|
406
|
+
// Text mode: pass through as-is
|
|
373
407
|
jsonSchema.yieldType = config.schema.yieldType
|
|
374
408
|
}
|
|
375
409
|
if (config.schema?.returnType) {
|