ts-procedures 4.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +17 -18
- package/build/implementations/http/hono-stream/index.js +38 -37
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +306 -61
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/hono-stream/types.d.ts +3 -3
- package/build/implementations/types.d.ts +5 -5
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/index.ts +1 -1
- package/src/implementations/http/hono-rpc/index.ts +1 -1
- package/src/implementations/http/hono-stream/README.md +151 -67
- package/src/implementations/http/hono-stream/index.test.ts +374 -66
- package/src/implementations/http/hono-stream/index.ts +62 -58
- package/src/implementations/http/hono-stream/types.ts +3 -3
- package/src/implementations/types.ts +5 -5
|
@@ -3,8 +3,9 @@ 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'
|
|
7
|
-
import { RPCConfig } from '../../types.js'
|
|
6
|
+
import { HonoStreamAppBuilder, sse, MidStreamErrorResult } from './index.js'
|
|
7
|
+
import { RPCConfig, StreamMode } from '../../types.js'
|
|
8
|
+
import { ProcedureValidationError } from '../../../errors.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* HonoStreamAppBuilder Test Suite
|
|
@@ -23,7 +24,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
23
24
|
|
|
24
25
|
RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* () {
|
|
25
26
|
yield { message: 'hello' }
|
|
26
|
-
yield {
|
|
27
|
+
yield { message: 'world' }
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
builder.register(RPC, () => ({ userId: '123' }))
|
|
@@ -45,7 +46,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
45
46
|
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
46
47
|
|
|
47
48
|
RPC.CreateStream('StreamData', { scope: 'data', version: 1 }, async function* () {
|
|
48
|
-
yield { data:
|
|
49
|
+
yield { data: 1 }
|
|
49
50
|
})
|
|
50
51
|
|
|
51
52
|
builder.register(RPC, () => ({ userId: '123' }))
|
|
@@ -84,9 +85,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
84
85
|
const RPC = Procedures<{}, RPCConfig>()
|
|
85
86
|
|
|
86
87
|
RPC.CreateStream('Counter', { scope: 'counter', version: 1 }, async function* () {
|
|
87
|
-
yield {
|
|
88
|
-
yield {
|
|
89
|
-
yield {
|
|
88
|
+
yield { count: 1 }
|
|
89
|
+
yield { count: 2 }
|
|
90
|
+
yield { count: 3 }
|
|
90
91
|
})
|
|
91
92
|
|
|
92
93
|
builder.register(RPC, () => ({}))
|
|
@@ -111,7 +112,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
111
112
|
const RPC = Procedures<{}, RPCConfig>()
|
|
112
113
|
|
|
113
114
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
114
|
-
yield {
|
|
115
|
+
yield { ok: true }
|
|
115
116
|
})
|
|
116
117
|
|
|
117
118
|
builder.register(RPC, () => ({}))
|
|
@@ -126,7 +127,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
126
127
|
const RPC = Procedures<{}, RPCConfig>()
|
|
127
128
|
|
|
128
129
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
129
|
-
yield {
|
|
130
|
+
yield { ok: true }
|
|
130
131
|
})
|
|
131
132
|
|
|
132
133
|
builder.register(RPC, () => ({}))
|
|
@@ -191,7 +192,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
191
192
|
const RPC = Procedures<{}, RPCConfig>()
|
|
192
193
|
|
|
193
194
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
194
|
-
yield {
|
|
195
|
+
yield { method: 'works' }
|
|
195
196
|
})
|
|
196
197
|
|
|
197
198
|
builder.register(RPC, () => ({}))
|
|
@@ -206,7 +207,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
206
207
|
const RPC = Procedures<{}, RPCConfig>()
|
|
207
208
|
|
|
208
209
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
209
|
-
yield {
|
|
210
|
+
yield { method: 'works' }
|
|
210
211
|
})
|
|
211
212
|
|
|
212
213
|
builder.register(RPC, () => ({}))
|
|
@@ -268,7 +269,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
268
269
|
const RPC = Procedures<{}, RPCConfig>()
|
|
269
270
|
|
|
270
271
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
271
|
-
yield {
|
|
272
|
+
yield { ok: true }
|
|
272
273
|
})
|
|
273
274
|
|
|
274
275
|
builder.register(RPC, () => ({}))
|
|
@@ -283,7 +284,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
283
284
|
const RPC = Procedures<{}, RPCConfig>()
|
|
284
285
|
|
|
285
286
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
286
|
-
yield {
|
|
287
|
+
yield { ok: true }
|
|
287
288
|
})
|
|
288
289
|
|
|
289
290
|
builder.register(RPC, () => ({}))
|
|
@@ -298,7 +299,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
298
299
|
const RPC = Procedures<{}, RPCConfig>()
|
|
299
300
|
|
|
300
301
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
301
|
-
yield {
|
|
302
|
+
yield {}
|
|
302
303
|
})
|
|
303
304
|
|
|
304
305
|
builder.register(RPC, () => ({}))
|
|
@@ -318,7 +319,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
318
319
|
const RPC = Procedures<{}, RPCConfig>()
|
|
319
320
|
|
|
320
321
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
321
|
-
yield {
|
|
322
|
+
yield { ok: true }
|
|
322
323
|
})
|
|
323
324
|
|
|
324
325
|
builder.register(RPC, () => ({}))
|
|
@@ -336,7 +337,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
336
337
|
const RPC = Procedures<{}, RPCConfig>()
|
|
337
338
|
|
|
338
339
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
339
|
-
yield {
|
|
340
|
+
yield { ok: true }
|
|
340
341
|
})
|
|
341
342
|
|
|
342
343
|
builder.register(RPC, () => ({}))
|
|
@@ -349,13 +350,13 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
349
350
|
expect(onRequestEnd.mock.calls[0]![0]).toHaveProperty('req')
|
|
350
351
|
})
|
|
351
352
|
|
|
352
|
-
test('onStreamStart is called before streaming begins', async () => {
|
|
353
|
+
test('onStreamStart is called before streaming begins with streamMode', async () => {
|
|
353
354
|
const onStreamStart = vi.fn()
|
|
354
355
|
const builder = new HonoStreamAppBuilder({ onStreamStart })
|
|
355
356
|
const RPC = Procedures<{}, RPCConfig>()
|
|
356
357
|
|
|
357
358
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
358
|
-
yield {
|
|
359
|
+
yield { ok: true }
|
|
359
360
|
})
|
|
360
361
|
|
|
361
362
|
builder.register(RPC, () => ({}))
|
|
@@ -365,15 +366,16 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
365
366
|
|
|
366
367
|
expect(onStreamStart).toHaveBeenCalledTimes(1)
|
|
367
368
|
expect(onStreamStart.mock.calls[0]![0]).toHaveProperty('name', 'Test')
|
|
369
|
+
expect(onStreamStart.mock.calls[0]![2]).toBe('sse')
|
|
368
370
|
})
|
|
369
371
|
|
|
370
|
-
test('onStreamEnd is called after stream completes', async () => {
|
|
372
|
+
test('onStreamEnd is called after stream completes with streamMode', async () => {
|
|
371
373
|
const onStreamEnd = vi.fn()
|
|
372
374
|
const builder = new HonoStreamAppBuilder({ onStreamEnd })
|
|
373
375
|
const RPC = Procedures<{}, RPCConfig>()
|
|
374
376
|
|
|
375
377
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
376
|
-
yield {
|
|
378
|
+
yield { ok: true }
|
|
377
379
|
})
|
|
378
380
|
|
|
379
381
|
builder.register(RPC, () => ({}))
|
|
@@ -385,6 +387,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
385
387
|
|
|
386
388
|
expect(onStreamEnd).toHaveBeenCalledTimes(1)
|
|
387
389
|
expect(onStreamEnd.mock.calls[0]![0]).toHaveProperty('name', 'Test')
|
|
390
|
+
expect(onStreamEnd.mock.calls[0]![2]).toBe('sse')
|
|
388
391
|
})
|
|
389
392
|
|
|
390
393
|
test('hooks execute in correct order', async () => {
|
|
@@ -400,7 +403,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
400
403
|
|
|
401
404
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
402
405
|
order.push('handler')
|
|
403
|
-
yield {
|
|
406
|
+
yield { ok: true }
|
|
404
407
|
})
|
|
405
408
|
|
|
406
409
|
builder.register(RPC, () => ({}))
|
|
@@ -448,7 +451,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
448
451
|
},
|
|
449
452
|
},
|
|
450
453
|
async function* (ctx, params) {
|
|
451
|
-
yield {
|
|
454
|
+
yield { count: params.count }
|
|
452
455
|
}
|
|
453
456
|
)
|
|
454
457
|
|
|
@@ -471,7 +474,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
471
474
|
const RPC = Procedures<{}, RPCConfig>()
|
|
472
475
|
|
|
473
476
|
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
474
|
-
yield {
|
|
477
|
+
yield { count: 1 }
|
|
475
478
|
throw new Error('Stream error')
|
|
476
479
|
})
|
|
477
480
|
|
|
@@ -521,7 +524,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
521
524
|
},
|
|
522
525
|
},
|
|
523
526
|
async function* (ctx, params) {
|
|
524
|
-
yield {
|
|
527
|
+
yield { count: params.count }
|
|
525
528
|
}
|
|
526
529
|
)
|
|
527
530
|
|
|
@@ -558,7 +561,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
558
561
|
},
|
|
559
562
|
},
|
|
560
563
|
async function* (ctx, params) {
|
|
561
|
-
yield {
|
|
564
|
+
yield { count: params.count }
|
|
562
565
|
}
|
|
563
566
|
)
|
|
564
567
|
|
|
@@ -584,7 +587,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
584
587
|
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
585
588
|
|
|
586
589
|
RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
|
|
587
|
-
yield {
|
|
590
|
+
yield { userId: ctx.userId }
|
|
588
591
|
})
|
|
589
592
|
|
|
590
593
|
builder.register(RPC, () => {
|
|
@@ -618,7 +621,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
618
621
|
const RPC = Procedures<{}, RPCConfig>()
|
|
619
622
|
|
|
620
623
|
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
621
|
-
yield {
|
|
624
|
+
yield { type: 'data', value: 1 }
|
|
622
625
|
throw new Error('Something broke')
|
|
623
626
|
})
|
|
624
627
|
|
|
@@ -794,14 +797,15 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
794
797
|
expect(doc.jsonSchema.params).toBeDefined()
|
|
795
798
|
expect(doc.jsonSchema.returnType).toBeDefined()
|
|
796
799
|
|
|
797
|
-
//
|
|
800
|
+
// yieldType is nested under SSE envelope's data property
|
|
798
801
|
const yt = doc.jsonSchema.yieldType as Record<string, any>
|
|
802
|
+
expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.')
|
|
799
803
|
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
800
804
|
expect(yt.properties.event).toEqual({ type: 'string' })
|
|
801
805
|
expect(yt.properties.id).toEqual({ type: 'string' })
|
|
802
806
|
expect(yt.properties.retry).toEqual({ type: 'number' })
|
|
803
|
-
// Developer's
|
|
804
|
-
expect(yt.properties.message).toBeDefined()
|
|
807
|
+
// Developer's yieldType is nested under data
|
|
808
|
+
expect(yt.properties.data.properties.message).toBeDefined()
|
|
805
809
|
})
|
|
806
810
|
|
|
807
811
|
test('SSE mode generates SSE envelope even when no yieldType is defined', () => {
|
|
@@ -818,7 +822,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
818
822
|
const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
|
|
819
823
|
expect(yt).toBeDefined()
|
|
820
824
|
expect(yt.type).toBe('object')
|
|
825
|
+
expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.')
|
|
821
826
|
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
827
|
+
// data is empty schema when no yieldType defined
|
|
822
828
|
expect(yt.properties.data).toEqual({})
|
|
823
829
|
expect(yt.properties.event).toEqual({ type: 'string' })
|
|
824
830
|
expect(yt.properties.id).toEqual({ type: 'string' })
|
|
@@ -849,25 +855,21 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
849
855
|
expect(yt.properties?.retry).toBeUndefined()
|
|
850
856
|
})
|
|
851
857
|
|
|
852
|
-
test('
|
|
858
|
+
test('yieldType with id property does not collide with SSE id field', () => {
|
|
859
|
+
// User's yieldType has an `id` field (number) — this should be nested under
|
|
860
|
+
// the SSE envelope's `data` property, not collide with the SSE `id` (string)
|
|
853
861
|
const yieldSchema = v.object({
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
id: v.string().const('fixed-id'),
|
|
862
|
+
id: v.number(),
|
|
863
|
+
message: v.string(),
|
|
857
864
|
})
|
|
858
865
|
const builder = new HonoStreamAppBuilder()
|
|
859
866
|
const RPC = Procedures<{}, RPCConfig>()
|
|
860
867
|
|
|
861
868
|
RPC.CreateStream(
|
|
862
|
-
'
|
|
869
|
+
'Notifications',
|
|
863
870
|
{ scope: 'test', version: 1, schema: { yieldType: yieldSchema } },
|
|
864
871
|
async function* () {
|
|
865
|
-
|
|
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
|
-
}
|
|
872
|
+
yield { id: 42, message: 'hello' }
|
|
871
873
|
}
|
|
872
874
|
)
|
|
873
875
|
|
|
@@ -876,11 +878,11 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
876
878
|
|
|
877
879
|
const yt = builder.docs[0]!.jsonSchema.yieldType as Record<string, any>
|
|
878
880
|
expect(yt.required).toEqual(['data', 'event', 'id'])
|
|
879
|
-
//
|
|
880
|
-
expect(yt.properties.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
expect(yt.properties.
|
|
881
|
+
// SSE envelope id is a string
|
|
882
|
+
expect(yt.properties.id).toEqual({ type: 'string' })
|
|
883
|
+
// User's id (number) is safely nested under data
|
|
884
|
+
expect(yt.properties.data.properties.id.type).toBe('number')
|
|
885
|
+
expect(yt.properties.data.properties.message.type).toBe('string')
|
|
884
886
|
})
|
|
885
887
|
|
|
886
888
|
test('streamMode is recorded in docs', () => {
|
|
@@ -927,7 +929,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
927
929
|
|
|
928
930
|
// Streaming procedure (should be registered)
|
|
929
931
|
RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
|
|
930
|
-
yield {
|
|
932
|
+
yield { ok: true }
|
|
931
933
|
})
|
|
932
934
|
|
|
933
935
|
builder.register(RPC, () => ({}))
|
|
@@ -956,7 +958,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
956
958
|
const RPC = Procedures<{}, RPCConfig>()
|
|
957
959
|
|
|
958
960
|
RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
|
|
959
|
-
yield {
|
|
961
|
+
yield {}
|
|
960
962
|
})
|
|
961
963
|
|
|
962
964
|
builder.register(RPC, () => ({}), {
|
|
@@ -979,7 +981,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
979
981
|
const RPC = Procedures<{}, RPCConfig>()
|
|
980
982
|
|
|
981
983
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
982
|
-
yield {
|
|
984
|
+
yield {}
|
|
983
985
|
})
|
|
984
986
|
|
|
985
987
|
builder.register(RPC, () => ({}), {
|
|
@@ -1050,7 +1052,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1050
1052
|
const TextRPC = Procedures<{}, RPCConfig>()
|
|
1051
1053
|
|
|
1052
1054
|
SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
|
|
1053
|
-
yield {
|
|
1055
|
+
yield { mode: 'sse' }
|
|
1054
1056
|
})
|
|
1055
1057
|
|
|
1056
1058
|
TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
|
|
@@ -1114,7 +1116,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1114
1116
|
|
|
1115
1117
|
RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
|
|
1116
1118
|
receivedIsPrevalidated = ctx.isPrevalidated
|
|
1117
|
-
yield {
|
|
1119
|
+
yield { ok: true }
|
|
1118
1120
|
})
|
|
1119
1121
|
|
|
1120
1122
|
builder.register(RPC, () => ({}))
|
|
@@ -1200,13 +1202,13 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1200
1202
|
// SSE Yield Shape Tests
|
|
1201
1203
|
// --------------------------------------------------------------------------
|
|
1202
1204
|
describe('SSE yield shape', () => {
|
|
1203
|
-
test('custom event names
|
|
1205
|
+
test('custom event names via sse() helper', async () => {
|
|
1204
1206
|
const builder = new HonoStreamAppBuilder()
|
|
1205
1207
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1206
1208
|
|
|
1207
1209
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1208
|
-
yield {
|
|
1209
|
-
yield {
|
|
1210
|
+
yield sse({ type: 'user_joined' }, { event: 'join' })
|
|
1211
|
+
yield sse({ type: 'message' }, { event: 'chat' })
|
|
1210
1212
|
})
|
|
1211
1213
|
|
|
1212
1214
|
builder.register(RPC, () => ({}))
|
|
@@ -1220,13 +1222,13 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1220
1222
|
expect(text).not.toContain('event: Events')
|
|
1221
1223
|
})
|
|
1222
1224
|
|
|
1223
|
-
test('custom id
|
|
1225
|
+
test('custom id via sse() helper', async () => {
|
|
1224
1226
|
const builder = new HonoStreamAppBuilder()
|
|
1225
1227
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1226
1228
|
|
|
1227
1229
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1228
|
-
yield {
|
|
1229
|
-
yield {
|
|
1230
|
+
yield sse({ msg: 'first' }, { id: 'msg-001' })
|
|
1231
|
+
yield sse({ msg: 'second' }, { id: 'msg-002' })
|
|
1230
1232
|
})
|
|
1231
1233
|
|
|
1232
1234
|
builder.register(RPC, () => ({}))
|
|
@@ -1244,8 +1246,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1244
1246
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1245
1247
|
|
|
1246
1248
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
1247
|
-
yield
|
|
1248
|
-
yield {
|
|
1249
|
+
yield 'already a string'
|
|
1250
|
+
yield { needs: 'stringify' }
|
|
1249
1251
|
})
|
|
1250
1252
|
|
|
1251
1253
|
builder.register(RPC, () => ({}))
|
|
@@ -1265,9 +1267,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1265
1267
|
const RPC = Procedures<{}, RPCConfig>()
|
|
1266
1268
|
|
|
1267
1269
|
RPC.CreateStream('MyProcedure', { scope: 'test', version: 1 }, async function* () {
|
|
1268
|
-
yield {
|
|
1269
|
-
yield {
|
|
1270
|
-
yield {
|
|
1270
|
+
yield { value: 1 }
|
|
1271
|
+
yield sse({ value: 2 }, { event: 'custom' })
|
|
1272
|
+
yield { value: 3 }
|
|
1271
1273
|
})
|
|
1272
1274
|
|
|
1273
1275
|
builder.register(RPC, () => ({}))
|
|
@@ -1288,6 +1290,312 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1288
1290
|
})
|
|
1289
1291
|
})
|
|
1290
1292
|
|
|
1293
|
+
// --------------------------------------------------------------------------
|
|
1294
|
+
// sse() Helper Tests
|
|
1295
|
+
// --------------------------------------------------------------------------
|
|
1296
|
+
describe('sse() helper', () => {
|
|
1297
|
+
test('tagged yields with custom event/id/retry', async () => {
|
|
1298
|
+
const builder = new HonoStreamAppBuilder()
|
|
1299
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1300
|
+
|
|
1301
|
+
RPC.CreateStream('Tagged', { scope: 'tagged', version: 1 }, async function* () {
|
|
1302
|
+
yield sse({ count: 1 }, { event: 'tick', id: 'evt-1', retry: 5000 })
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
builder.register(RPC, () => ({}))
|
|
1306
|
+
const app = builder.build()
|
|
1307
|
+
|
|
1308
|
+
const res = await app.request('/tagged/tagged/1')
|
|
1309
|
+
const text = await res.text()
|
|
1310
|
+
|
|
1311
|
+
expect(text).toContain('event: tick')
|
|
1312
|
+
expect(text).toContain('id: evt-1')
|
|
1313
|
+
expect(text).toContain('retry: 5000')
|
|
1314
|
+
expect(text).toContain('data: {"count":1}')
|
|
1315
|
+
})
|
|
1316
|
+
|
|
1317
|
+
test('plain domain objects use procedure name and auto-incremented id', async () => {
|
|
1318
|
+
const builder = new HonoStreamAppBuilder()
|
|
1319
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1320
|
+
|
|
1321
|
+
RPC.CreateStream('Plain', { scope: 'plain', version: 1 }, async function* () {
|
|
1322
|
+
yield { a: 1 }
|
|
1323
|
+
yield { a: 2 }
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
builder.register(RPC, () => ({}))
|
|
1327
|
+
const app = builder.build()
|
|
1328
|
+
|
|
1329
|
+
const res = await app.request('/plain/plain/1')
|
|
1330
|
+
const text = await res.text()
|
|
1331
|
+
|
|
1332
|
+
const messages = text.split('\n\n').filter(Boolean)
|
|
1333
|
+
expect(messages[0]).toContain('event: Plain')
|
|
1334
|
+
expect(messages[0]).toContain('id: 0')
|
|
1335
|
+
expect(messages[0]).toContain('data: {"a":1}')
|
|
1336
|
+
expect(messages[1]).toContain('event: Plain')
|
|
1337
|
+
expect(messages[1]).toContain('id: 1')
|
|
1338
|
+
expect(messages[1]).toContain('data: {"a":2}')
|
|
1339
|
+
})
|
|
1340
|
+
|
|
1341
|
+
test('sse() metadata is invisible in text mode', async () => {
|
|
1342
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' })
|
|
1343
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1344
|
+
|
|
1345
|
+
RPC.CreateStream('TextTagged', { scope: 'text', version: 1 }, async function* () {
|
|
1346
|
+
yield sse({ count: 1 }, { event: 'tick' })
|
|
1347
|
+
yield { count: 2 }
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
builder.register(RPC, () => ({}))
|
|
1351
|
+
const app = builder.build()
|
|
1352
|
+
|
|
1353
|
+
const res = await app.request('/text/text-tagged/1')
|
|
1354
|
+
const text = await res.text()
|
|
1355
|
+
const lines = text.trim().split('\n')
|
|
1356
|
+
|
|
1357
|
+
// Text mode just JSON-stringifies — sse() metadata is not visible
|
|
1358
|
+
expect(JSON.parse(lines[0]!)).toEqual({ count: 1 })
|
|
1359
|
+
expect(JSON.parse(lines[1]!)).toEqual({ count: 2 })
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
test('sse() with partial options', async () => {
|
|
1363
|
+
const builder = new HonoStreamAppBuilder()
|
|
1364
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1365
|
+
|
|
1366
|
+
RPC.CreateStream('Partial', { scope: 'partial', version: 1 }, async function* () {
|
|
1367
|
+
yield sse({ v: 1 }, { event: 'custom' })
|
|
1368
|
+
yield sse({ v: 2 }, { id: 'my-id' })
|
|
1369
|
+
yield sse({ v: 3 })
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
builder.register(RPC, () => ({}))
|
|
1373
|
+
const app = builder.build()
|
|
1374
|
+
|
|
1375
|
+
const res = await app.request('/partial/partial/1')
|
|
1376
|
+
const text = await res.text()
|
|
1377
|
+
const messages = text.split('\n\n').filter(Boolean)
|
|
1378
|
+
|
|
1379
|
+
// First: custom event, auto id
|
|
1380
|
+
expect(messages[0]).toContain('event: custom')
|
|
1381
|
+
expect(messages[0]).toContain('id: 0')
|
|
1382
|
+
|
|
1383
|
+
// Second: default event, custom id
|
|
1384
|
+
expect(messages[1]).toContain('event: Partial')
|
|
1385
|
+
expect(messages[1]).toContain('id: my-id')
|
|
1386
|
+
|
|
1387
|
+
// Third: sse() with no options — same as plain object (defaults)
|
|
1388
|
+
expect(messages[2]).toContain('event: Partial')
|
|
1389
|
+
expect(messages[2]).toContain('id: 2')
|
|
1390
|
+
})
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
// --------------------------------------------------------------------------
|
|
1394
|
+
// streamMode in Lifecycle Hooks
|
|
1395
|
+
// --------------------------------------------------------------------------
|
|
1396
|
+
describe('streamMode in lifecycle hooks', () => {
|
|
1397
|
+
test('onStreamStart receives sse streamMode', async () => {
|
|
1398
|
+
const onStreamStart = vi.fn()
|
|
1399
|
+
const builder = new HonoStreamAppBuilder({ onStreamStart })
|
|
1400
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1401
|
+
|
|
1402
|
+
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
1403
|
+
yield { ok: true }
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
builder.register(RPC, () => ({}))
|
|
1407
|
+
const app = builder.build()
|
|
1408
|
+
|
|
1409
|
+
await app.request('/test/test/1')
|
|
1410
|
+
|
|
1411
|
+
expect(onStreamStart).toHaveBeenCalledTimes(1)
|
|
1412
|
+
const [, , streamMode] = onStreamStart.mock.calls[0]!
|
|
1413
|
+
expect(streamMode).toBe('sse')
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
test('onStreamEnd receives text streamMode', async () => {
|
|
1417
|
+
const onStreamEnd = vi.fn()
|
|
1418
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text', onStreamEnd })
|
|
1419
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1420
|
+
|
|
1421
|
+
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
1422
|
+
yield { ok: true }
|
|
1423
|
+
})
|
|
1424
|
+
|
|
1425
|
+
builder.register(RPC, () => ({}))
|
|
1426
|
+
const app = builder.build()
|
|
1427
|
+
|
|
1428
|
+
const res = await app.request('/test/test/1')
|
|
1429
|
+
await res.text()
|
|
1430
|
+
|
|
1431
|
+
expect(onStreamEnd).toHaveBeenCalledTimes(1)
|
|
1432
|
+
const [, , streamMode] = onStreamEnd.mock.calls[0]!
|
|
1433
|
+
expect(streamMode).toBe('text')
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
test('onStreamStart and onStreamEnd receive matching streamMode', async () => {
|
|
1437
|
+
const modes: { start?: StreamMode; end?: StreamMode } = {}
|
|
1438
|
+
const builder = new HonoStreamAppBuilder({
|
|
1439
|
+
defaultStreamMode: 'text',
|
|
1440
|
+
onStreamStart: (_proc, _c, mode) => { modes.start = mode },
|
|
1441
|
+
onStreamEnd: (_proc, _c, mode) => { modes.end = mode },
|
|
1442
|
+
})
|
|
1443
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1444
|
+
|
|
1445
|
+
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
1446
|
+
yield { ok: true }
|
|
1447
|
+
})
|
|
1448
|
+
|
|
1449
|
+
builder.register(RPC, () => ({}))
|
|
1450
|
+
const app = builder.build()
|
|
1451
|
+
|
|
1452
|
+
const res = await app.request('/test/test/1')
|
|
1453
|
+
await res.text()
|
|
1454
|
+
|
|
1455
|
+
expect(modes.start).toBe('text')
|
|
1456
|
+
expect(modes.end).toBe('text')
|
|
1457
|
+
})
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
// --------------------------------------------------------------------------
|
|
1461
|
+
// sse() in onMidStreamError
|
|
1462
|
+
// --------------------------------------------------------------------------
|
|
1463
|
+
describe('sse() in onMidStreamError', () => {
|
|
1464
|
+
test('sse() wraps error data with custom event and id', async () => {
|
|
1465
|
+
const builder = new HonoStreamAppBuilder({
|
|
1466
|
+
onMidStreamError: (procedure, c, error) => {
|
|
1467
|
+
return {
|
|
1468
|
+
data: sse(
|
|
1469
|
+
{ type: 'error', message: error.message },
|
|
1470
|
+
{ event: 'custom-error', id: 'err-1' }
|
|
1471
|
+
),
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
})
|
|
1475
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1476
|
+
|
|
1477
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
1478
|
+
yield { type: 'data', value: 1 }
|
|
1479
|
+
throw new Error('Something broke')
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
builder.register(RPC, () => ({}))
|
|
1483
|
+
const app = builder.build()
|
|
1484
|
+
|
|
1485
|
+
const res = await app.request('/error/error-stream/1')
|
|
1486
|
+
const text = await res.text()
|
|
1487
|
+
|
|
1488
|
+
// Normal yield
|
|
1489
|
+
expect(text).toContain('data: {"type":"data","value":1}')
|
|
1490
|
+
// Error yield with sse() metadata
|
|
1491
|
+
expect(text).toContain('event: custom-error')
|
|
1492
|
+
expect(text).toContain('id: err-1')
|
|
1493
|
+
expect(text).toContain('"type":"error"')
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
test('string error data without sse() uses default event and id', async () => {
|
|
1497
|
+
const builder = new HonoStreamAppBuilder({
|
|
1498
|
+
onMidStreamError: () => {
|
|
1499
|
+
return { data: 'plain error string' }
|
|
1500
|
+
},
|
|
1501
|
+
})
|
|
1502
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1503
|
+
|
|
1504
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
1505
|
+
yield { count: 1 }
|
|
1506
|
+
throw new Error('fail')
|
|
1507
|
+
})
|
|
1508
|
+
|
|
1509
|
+
builder.register(RPC, () => ({}))
|
|
1510
|
+
const app = builder.build()
|
|
1511
|
+
|
|
1512
|
+
const res = await app.request('/error/error-stream/1')
|
|
1513
|
+
const text = await res.text()
|
|
1514
|
+
|
|
1515
|
+
// String data can't use sse() (not an object), so defaults apply
|
|
1516
|
+
expect(text).toContain('data: plain error string')
|
|
1517
|
+
// event defaults to procedure name when data is provided
|
|
1518
|
+
expect(text).toContain('event: ErrorStream')
|
|
1519
|
+
})
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
// --------------------------------------------------------------------------
|
|
1523
|
+
// Generic TErrorData
|
|
1524
|
+
// --------------------------------------------------------------------------
|
|
1525
|
+
describe('generic TErrorData', () => {
|
|
1526
|
+
test('typed builder constrains onMidStreamError return type', async () => {
|
|
1527
|
+
type ErrorPayload = { type: 'error'; code: string; message: string }
|
|
1528
|
+
|
|
1529
|
+
const builder = new HonoStreamAppBuilder<ErrorPayload>({
|
|
1530
|
+
onMidStreamError: (_procedure, _c, error) => {
|
|
1531
|
+
// This satisfies MidStreamErrorResult<ErrorPayload>
|
|
1532
|
+
return {
|
|
1533
|
+
data: { type: 'error', code: 'STREAM_FAILED', message: error.message },
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
})
|
|
1537
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1538
|
+
|
|
1539
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
1540
|
+
yield { value: 1 }
|
|
1541
|
+
throw new Error('typed error')
|
|
1542
|
+
})
|
|
1543
|
+
|
|
1544
|
+
builder.register(RPC, () => ({}))
|
|
1545
|
+
const app = builder.build()
|
|
1546
|
+
|
|
1547
|
+
const res = await app.request('/error/error-stream/1')
|
|
1548
|
+
const text = await res.text()
|
|
1549
|
+
|
|
1550
|
+
expect(text).toContain('"code":"STREAM_FAILED"')
|
|
1551
|
+
// Error message may be wrapped by Procedures with prefix
|
|
1552
|
+
expect(text).toContain('typed error')
|
|
1553
|
+
})
|
|
1554
|
+
})
|
|
1555
|
+
|
|
1556
|
+
// --------------------------------------------------------------------------
|
|
1557
|
+
// ProcedureValidationError narrowing in onPreStreamError
|
|
1558
|
+
// --------------------------------------------------------------------------
|
|
1559
|
+
describe('ProcedureValidationError narrowing', () => {
|
|
1560
|
+
test('instanceof check works in onPreStreamError', async () => {
|
|
1561
|
+
let wasValidationError = false
|
|
1562
|
+
|
|
1563
|
+
const builder = new HonoStreamAppBuilder({
|
|
1564
|
+
onPreStreamError: (procedure, c, error) => {
|
|
1565
|
+
if (error instanceof ProcedureValidationError) {
|
|
1566
|
+
wasValidationError = true
|
|
1567
|
+
return c.json({ validation: true, errors: error.errors }, 422)
|
|
1568
|
+
}
|
|
1569
|
+
return c.json({ error: error.message }, 500)
|
|
1570
|
+
},
|
|
1571
|
+
})
|
|
1572
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
1573
|
+
|
|
1574
|
+
RPC.CreateStream(
|
|
1575
|
+
'Validated',
|
|
1576
|
+
{
|
|
1577
|
+
scope: 'validated',
|
|
1578
|
+
version: 1,
|
|
1579
|
+
schema: { params: v.object({ count: v.number() }) },
|
|
1580
|
+
},
|
|
1581
|
+
async function* (ctx, params) {
|
|
1582
|
+
yield { count: params.count }
|
|
1583
|
+
}
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
builder.register(RPC, () => ({}))
|
|
1587
|
+
const app = builder.build()
|
|
1588
|
+
|
|
1589
|
+
const res = await app.request('/validated/validated/1?count=not-a-number')
|
|
1590
|
+
|
|
1591
|
+
expect(res.status).toBe(422)
|
|
1592
|
+
expect(wasValidationError).toBe(true)
|
|
1593
|
+
const body = await res.json()
|
|
1594
|
+
expect(body.validation).toBe(true)
|
|
1595
|
+
expect(body.errors).toBeDefined()
|
|
1596
|
+
})
|
|
1597
|
+
})
|
|
1598
|
+
|
|
1291
1599
|
// --------------------------------------------------------------------------
|
|
1292
1600
|
// Integration Test
|
|
1293
1601
|
// --------------------------------------------------------------------------
|
|
@@ -1330,8 +1638,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1330
1638
|
defaultStreamMode: 'text',
|
|
1331
1639
|
onRequestStart: () => events.push('request-start'),
|
|
1332
1640
|
onRequestEnd: () => events.push('request-end'),
|
|
1333
|
-
onStreamStart: (
|
|
1334
|
-
onStreamEnd: (
|
|
1641
|
+
onStreamStart: () => events.push('stream-start'),
|
|
1642
|
+
onStreamEnd: () => events.push('stream-end'),
|
|
1335
1643
|
})
|
|
1336
1644
|
|
|
1337
1645
|
builder.register(RPC, (c) => ({
|
|
@@ -1360,8 +1668,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
1360
1668
|
|
|
1361
1669
|
// Verify hooks were called
|
|
1362
1670
|
expect(events).toContain('request-start')
|
|
1363
|
-
expect(events).toContain('stream-start
|
|
1364
|
-
expect(events).toContain('stream-end
|
|
1671
|
+
expect(events).toContain('stream-start')
|
|
1672
|
+
expect(events).toContain('stream-end')
|
|
1365
1673
|
expect(events).toContain('request-end')
|
|
1366
1674
|
})
|
|
1367
1675
|
})
|