ts-procedures 3.3.4 → 4.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 +31 -2
- package/build/implementations/http/hono-stream/index.js +36 -25
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +229 -47
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/implementations/http/README.md +2 -1
- package/src/implementations/http/hono-stream/README.md +159 -28
- package/src/implementations/http/hono-stream/index.test.ts +292 -56
- package/src/implementations/http/hono-stream/index.ts +78 -27
|
@@ -19,7 +19,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
19
19
|
const RPC = Procedures();
|
|
20
20
|
RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* (ctx) {
|
|
21
21
|
yield { message: 'hello' };
|
|
22
|
-
yield { message: 'world' };
|
|
22
|
+
yield { data: { message: 'world' } };
|
|
23
23
|
});
|
|
24
24
|
builder.register(RPC, () => ({ userId: '123' }));
|
|
25
25
|
const app = builder.build();
|
|
@@ -35,7 +35,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
35
35
|
const builder = new HonoStreamAppBuilder({ app: customApp });
|
|
36
36
|
const RPC = Procedures();
|
|
37
37
|
RPC.CreateStream('StreamData', { scope: 'data', version: 1 }, async function* () {
|
|
38
|
-
yield { data: 1 };
|
|
38
|
+
yield { data: { data: 1 } };
|
|
39
39
|
});
|
|
40
40
|
builder.register(RPC, () => ({ userId: '123' }));
|
|
41
41
|
const app = builder.build();
|
|
@@ -67,9 +67,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
67
67
|
const builder = new HonoStreamAppBuilder();
|
|
68
68
|
const RPC = Procedures();
|
|
69
69
|
RPC.CreateStream('Counter', { scope: 'counter', version: 1 }, async function* () {
|
|
70
|
-
yield { count: 1 };
|
|
71
|
-
yield { count: 2 };
|
|
72
|
-
yield { count: 3 };
|
|
70
|
+
yield { data: { count: 1 } };
|
|
71
|
+
yield { data: { count: 2 } };
|
|
72
|
+
yield { data: { count: 3 } };
|
|
73
73
|
});
|
|
74
74
|
builder.register(RPC, () => ({}));
|
|
75
75
|
const app = builder.build();
|
|
@@ -89,7 +89,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
89
89
|
const builder = new HonoStreamAppBuilder();
|
|
90
90
|
const RPC = Procedures();
|
|
91
91
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
92
|
-
yield { ok: true };
|
|
92
|
+
yield { data: { ok: true } };
|
|
93
93
|
});
|
|
94
94
|
builder.register(RPC, () => ({}));
|
|
95
95
|
const app = builder.build();
|
|
@@ -100,7 +100,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
100
100
|
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' });
|
|
101
101
|
const RPC = Procedures();
|
|
102
102
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
103
|
-
yield { ok: true };
|
|
103
|
+
yield { data: { ok: true } };
|
|
104
104
|
});
|
|
105
105
|
builder.register(RPC, () => ({}));
|
|
106
106
|
const app = builder.build();
|
|
@@ -152,7 +152,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
152
152
|
const builder = new HonoStreamAppBuilder();
|
|
153
153
|
const RPC = Procedures();
|
|
154
154
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
155
|
-
yield { method: 'works' };
|
|
155
|
+
yield { data: { method: 'works' } };
|
|
156
156
|
});
|
|
157
157
|
builder.register(RPC, () => ({}));
|
|
158
158
|
const app = builder.build();
|
|
@@ -163,7 +163,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
163
163
|
const builder = new HonoStreamAppBuilder();
|
|
164
164
|
const RPC = Procedures();
|
|
165
165
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
166
|
-
yield { method: 'works' };
|
|
166
|
+
yield { data: { method: 'works' } };
|
|
167
167
|
});
|
|
168
168
|
builder.register(RPC, () => ({}));
|
|
169
169
|
const app = builder.build();
|
|
@@ -213,7 +213,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
213
213
|
const builder = new HonoStreamAppBuilder({ pathPrefix: '/api/v1' });
|
|
214
214
|
const RPC = Procedures();
|
|
215
215
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
216
|
-
yield { ok: true };
|
|
216
|
+
yield { data: { ok: true } };
|
|
217
217
|
});
|
|
218
218
|
builder.register(RPC, () => ({}));
|
|
219
219
|
const app = builder.build();
|
|
@@ -224,7 +224,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
224
224
|
const builder = new HonoStreamAppBuilder({ pathPrefix: 'custom' });
|
|
225
225
|
const RPC = Procedures();
|
|
226
226
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
227
|
-
yield { ok: true };
|
|
227
|
+
yield { data: { ok: true } };
|
|
228
228
|
});
|
|
229
229
|
builder.register(RPC, () => ({}));
|
|
230
230
|
const app = builder.build();
|
|
@@ -235,7 +235,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
235
235
|
const builder = new HonoStreamAppBuilder({ pathPrefix: '/api' });
|
|
236
236
|
const RPC = Procedures();
|
|
237
237
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
238
|
-
yield {};
|
|
238
|
+
yield { data: {} };
|
|
239
239
|
});
|
|
240
240
|
builder.register(RPC, () => ({}));
|
|
241
241
|
builder.build();
|
|
@@ -251,7 +251,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
251
251
|
const builder = new HonoStreamAppBuilder({ onRequestStart });
|
|
252
252
|
const RPC = Procedures();
|
|
253
253
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
254
|
-
yield { ok: true };
|
|
254
|
+
yield { data: { ok: true } };
|
|
255
255
|
});
|
|
256
256
|
builder.register(RPC, () => ({}));
|
|
257
257
|
const app = builder.build();
|
|
@@ -264,11 +264,12 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
264
264
|
const builder = new HonoStreamAppBuilder({ onRequestEnd });
|
|
265
265
|
const RPC = Procedures();
|
|
266
266
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
267
|
-
yield { ok: true };
|
|
267
|
+
yield { data: { ok: true } };
|
|
268
268
|
});
|
|
269
269
|
builder.register(RPC, () => ({}));
|
|
270
270
|
const app = builder.build();
|
|
271
|
-
await app.request('/test/test/1');
|
|
271
|
+
const response = await app.request('/test/test/1');
|
|
272
|
+
await response.text(); // Consume stream to trigger onRequestEnd
|
|
272
273
|
expect(onRequestEnd).toHaveBeenCalledTimes(1);
|
|
273
274
|
expect(onRequestEnd.mock.calls[0][0]).toHaveProperty('req');
|
|
274
275
|
});
|
|
@@ -277,7 +278,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
277
278
|
const builder = new HonoStreamAppBuilder({ onStreamStart });
|
|
278
279
|
const RPC = Procedures();
|
|
279
280
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
280
|
-
yield { ok: true };
|
|
281
|
+
yield { data: { ok: true } };
|
|
281
282
|
});
|
|
282
283
|
builder.register(RPC, () => ({}));
|
|
283
284
|
const app = builder.build();
|
|
@@ -290,7 +291,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
290
291
|
const builder = new HonoStreamAppBuilder({ onStreamEnd });
|
|
291
292
|
const RPC = Procedures();
|
|
292
293
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
293
|
-
yield { ok: true };
|
|
294
|
+
yield { data: { ok: true } };
|
|
294
295
|
});
|
|
295
296
|
builder.register(RPC, () => ({}));
|
|
296
297
|
const app = builder.build();
|
|
@@ -311,7 +312,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
311
312
|
const RPC = Procedures();
|
|
312
313
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
313
314
|
order.push('handler');
|
|
314
|
-
yield { ok: true };
|
|
315
|
+
yield { data: { ok: true } };
|
|
315
316
|
});
|
|
316
317
|
builder.register(RPC, () => ({}));
|
|
317
318
|
const app = builder.build();
|
|
@@ -329,6 +330,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
329
330
|
expect(order[0]).toBe('request-start');
|
|
330
331
|
// stream-start should be before handler
|
|
331
332
|
expect(order.indexOf('stream-start')).toBeLessThan(order.indexOf('handler'));
|
|
333
|
+
// request-end should be last
|
|
334
|
+
expect(order[order.length - 1]).toBe('request-end');
|
|
332
335
|
});
|
|
333
336
|
});
|
|
334
337
|
// --------------------------------------------------------------------------
|
|
@@ -339,21 +342,32 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
339
342
|
const errorHandler = vi.fn((procedure, c, error) => {
|
|
340
343
|
return c.json({ customError: error.message }, 400);
|
|
341
344
|
});
|
|
342
|
-
const builder = new HonoStreamAppBuilder({
|
|
345
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError: errorHandler });
|
|
343
346
|
const RPC = Procedures();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
RPC.CreateStream('ValidatedStream', {
|
|
348
|
+
scope: 'validated',
|
|
349
|
+
version: 1,
|
|
350
|
+
schema: {
|
|
351
|
+
params: v.object({ count: v.number() }),
|
|
352
|
+
},
|
|
353
|
+
}, async function* (ctx, params) {
|
|
354
|
+
yield { data: { count: params.count } };
|
|
347
355
|
});
|
|
356
|
+
builder.register(RPC, () => ({}));
|
|
348
357
|
const app = builder.build();
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
359
|
+
expect(res.status).toBe(400);
|
|
360
|
+
const body = await res.json();
|
|
361
|
+
expect(body.customError).toContain('Validation error');
|
|
362
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
363
|
+
expect(errorHandler.mock.calls[0][0].name).toBe('ValidatedStream');
|
|
364
|
+
expect(errorHandler.mock.calls[0][2].message).toContain('Validation error');
|
|
351
365
|
});
|
|
352
366
|
test('errors during streaming are sent as error events (SSE mode)', async () => {
|
|
353
367
|
const builder = new HonoStreamAppBuilder();
|
|
354
368
|
const RPC = Procedures();
|
|
355
369
|
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
356
|
-
yield { count: 1 };
|
|
370
|
+
yield { data: { count: 1 } };
|
|
357
371
|
throw new Error('Stream error');
|
|
358
372
|
});
|
|
359
373
|
builder.register(RPC, () => ({}));
|
|
@@ -380,11 +394,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
380
394
|
// Error is wrapped by Procedures with "Error in streaming handler for {name}" prefix
|
|
381
395
|
expect(JSON.parse(lines[1]).error).toContain('Stream error');
|
|
382
396
|
});
|
|
383
|
-
test('
|
|
384
|
-
const
|
|
385
|
-
return c.json({ error: error.message }, 400);
|
|
386
|
-
});
|
|
387
|
-
const builder = new HonoStreamAppBuilder({ onStreamError });
|
|
397
|
+
test('validation errors return 400 by default when no error handler', async () => {
|
|
398
|
+
const builder = new HonoStreamAppBuilder();
|
|
388
399
|
const RPC = Procedures();
|
|
389
400
|
RPC.CreateStream('ValidatedStream', {
|
|
390
401
|
scope: 'validated',
|
|
@@ -393,22 +404,22 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
393
404
|
params: v.object({ count: v.number() }),
|
|
394
405
|
},
|
|
395
406
|
}, async function* (ctx, params) {
|
|
396
|
-
yield { count: params.count };
|
|
407
|
+
yield { data: { count: params.count } };
|
|
397
408
|
});
|
|
398
409
|
builder.register(RPC, () => ({}));
|
|
399
410
|
const app = builder.build();
|
|
400
411
|
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
401
|
-
//
|
|
412
|
+
// Default: returns 400 JSON error
|
|
402
413
|
expect(res.status).toBe(400);
|
|
403
414
|
const body = await res.json();
|
|
404
415
|
expect(body.error).toContain('Validation error');
|
|
405
|
-
// onStreamError callback was called
|
|
406
|
-
expect(onStreamError).toHaveBeenCalledTimes(1);
|
|
407
|
-
expect(onStreamError.mock.calls[0][0].name).toBe('ValidatedStream');
|
|
408
|
-
expect(onStreamError.mock.calls[0][2].message).toContain('Validation error');
|
|
409
416
|
});
|
|
410
|
-
|
|
411
|
-
|
|
417
|
+
// Tests for onPreStreamError and onMidStreamError callbacks
|
|
418
|
+
test('onPreStreamError handles validation errors with custom Response', async () => {
|
|
419
|
+
const onPreStreamError = vi.fn((procedure, c, error) => {
|
|
420
|
+
return c.json({ customError: true, procedureName: procedure.name, details: error.message }, 422);
|
|
421
|
+
});
|
|
422
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError });
|
|
412
423
|
const RPC = Procedures();
|
|
413
424
|
RPC.CreateStream('ValidatedStream', {
|
|
414
425
|
scope: 'validated',
|
|
@@ -417,15 +428,115 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
417
428
|
params: v.object({ count: v.number() }),
|
|
418
429
|
},
|
|
419
430
|
}, async function* (ctx, params) {
|
|
420
|
-
yield { count: params.count };
|
|
431
|
+
yield { data: { count: params.count } };
|
|
421
432
|
});
|
|
422
433
|
builder.register(RPC, () => ({}));
|
|
423
434
|
const app = builder.build();
|
|
424
435
|
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
425
|
-
|
|
426
|
-
expect(res.status).toBe(400);
|
|
436
|
+
expect(res.status).toBe(422);
|
|
427
437
|
const body = await res.json();
|
|
428
|
-
expect(body.
|
|
438
|
+
expect(body.customError).toBe(true);
|
|
439
|
+
expect(body.procedureName).toBe('ValidatedStream');
|
|
440
|
+
expect(body.details).toContain('Validation error');
|
|
441
|
+
expect(onPreStreamError).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
test('onPreStreamError handles context resolution errors', async () => {
|
|
444
|
+
const onPreStreamError = vi.fn((procedure, c, error) => {
|
|
445
|
+
return c.json({ contextError: error.message }, 401);
|
|
446
|
+
});
|
|
447
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError });
|
|
448
|
+
const RPC = Procedures();
|
|
449
|
+
RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
|
|
450
|
+
yield { data: { userId: ctx.userId } };
|
|
451
|
+
});
|
|
452
|
+
builder.register(RPC, () => {
|
|
453
|
+
throw new Error('Authentication required');
|
|
454
|
+
});
|
|
455
|
+
const app = builder.build();
|
|
456
|
+
const res = await app.request('/secure/secure-stream/1');
|
|
457
|
+
expect(res.status).toBe(401);
|
|
458
|
+
const body = await res.json();
|
|
459
|
+
expect(body.contextError).toBe('Authentication required');
|
|
460
|
+
expect(onPreStreamError).toHaveBeenCalledTimes(1);
|
|
461
|
+
});
|
|
462
|
+
test('onMidStreamError returns custom value written to SSE stream', async () => {
|
|
463
|
+
const onMidStreamError = vi.fn((procedure, c, error) => {
|
|
464
|
+
return {
|
|
465
|
+
data: {
|
|
466
|
+
type: 'error',
|
|
467
|
+
code: 'STREAM_FAILED',
|
|
468
|
+
message: error.message,
|
|
469
|
+
retryable: false,
|
|
470
|
+
},
|
|
471
|
+
closeStream: true,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
const builder = new HonoStreamAppBuilder({ onMidStreamError });
|
|
475
|
+
const RPC = Procedures();
|
|
476
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
477
|
+
yield { data: { type: 'data', value: 1 } };
|
|
478
|
+
throw new Error('Something broke');
|
|
479
|
+
});
|
|
480
|
+
builder.register(RPC, () => ({}));
|
|
481
|
+
const app = builder.build();
|
|
482
|
+
const res = await app.request('/error/error-stream/1');
|
|
483
|
+
const text = await res.text();
|
|
484
|
+
// First yield should be present
|
|
485
|
+
expect(text).toContain('data: {"type":"data","value":1}');
|
|
486
|
+
// Error should use custom format from onMidStreamError
|
|
487
|
+
expect(text).toContain('data: {"type":"error","code":"STREAM_FAILED"');
|
|
488
|
+
expect(text).toContain('"retryable":false');
|
|
489
|
+
// Event should use procedure name (not 'error') since custom value provided
|
|
490
|
+
expect(text).toContain('event: ErrorStream');
|
|
491
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
492
|
+
});
|
|
493
|
+
test('onMidStreamError returns custom value written to text stream', async () => {
|
|
494
|
+
const onMidStreamError = vi.fn((procedure, c, error) => {
|
|
495
|
+
return {
|
|
496
|
+
data: { type: 'error', message: error.message },
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
const builder = new HonoStreamAppBuilder({
|
|
500
|
+
defaultStreamMode: 'text',
|
|
501
|
+
onMidStreamError,
|
|
502
|
+
});
|
|
503
|
+
const RPC = Procedures();
|
|
504
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
505
|
+
yield { type: 'data', value: 'hello' };
|
|
506
|
+
throw new Error('Stream failed');
|
|
507
|
+
});
|
|
508
|
+
builder.register(RPC, () => ({}));
|
|
509
|
+
const app = builder.build();
|
|
510
|
+
const res = await app.request('/error/error-stream/1');
|
|
511
|
+
const text = await res.text();
|
|
512
|
+
const lines = text.trim().split('\n');
|
|
513
|
+
expect(JSON.parse(lines[0])).toEqual({ type: 'data', value: 'hello' });
|
|
514
|
+
// Error message may be wrapped by Procedures with "Error in streaming handler for X - " prefix
|
|
515
|
+
const errorLine = JSON.parse(lines[1]);
|
|
516
|
+
expect(errorLine.type).toBe('error');
|
|
517
|
+
expect(errorLine.message).toContain('Stream failed');
|
|
518
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
519
|
+
});
|
|
520
|
+
test('onMidStreamError returning undefined falls back to default error format', async () => {
|
|
521
|
+
const onMidStreamError = vi.fn(() => undefined);
|
|
522
|
+
const builder = new HonoStreamAppBuilder({
|
|
523
|
+
defaultStreamMode: 'text',
|
|
524
|
+
onMidStreamError,
|
|
525
|
+
});
|
|
526
|
+
const RPC = Procedures();
|
|
527
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
528
|
+
yield { value: 1 };
|
|
529
|
+
throw new Error('Fallback test');
|
|
530
|
+
});
|
|
531
|
+
builder.register(RPC, () => ({}));
|
|
532
|
+
const app = builder.build();
|
|
533
|
+
const res = await app.request('/error/error-stream/1');
|
|
534
|
+
const text = await res.text();
|
|
535
|
+
const lines = text.trim().split('\n');
|
|
536
|
+
expect(JSON.parse(lines[0])).toEqual({ value: 1 });
|
|
537
|
+
// Falls back to default { error: message } format
|
|
538
|
+
expect(JSON.parse(lines[1]).error).toContain('Fallback test');
|
|
539
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
429
540
|
});
|
|
430
541
|
});
|
|
431
542
|
// --------------------------------------------------------------------------
|
|
@@ -535,7 +646,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
535
646
|
}));
|
|
536
647
|
// Streaming procedure (should be registered)
|
|
537
648
|
RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
|
|
538
|
-
yield { ok: true };
|
|
649
|
+
yield { data: { ok: true } };
|
|
539
650
|
});
|
|
540
651
|
builder.register(RPC, () => ({}));
|
|
541
652
|
const app = builder.build();
|
|
@@ -558,7 +669,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
558
669
|
const builder = new HonoStreamAppBuilder();
|
|
559
670
|
const RPC = Procedures();
|
|
560
671
|
RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
|
|
561
|
-
yield {};
|
|
672
|
+
yield { data: {} };
|
|
562
673
|
});
|
|
563
674
|
builder.register(RPC, () => ({}), {
|
|
564
675
|
extendProcedureDoc: ({ base, procedure }) => ({
|
|
@@ -577,7 +688,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
577
688
|
const builder = new HonoStreamAppBuilder();
|
|
578
689
|
const RPC = Procedures();
|
|
579
690
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
580
|
-
yield {};
|
|
691
|
+
yield { data: {} };
|
|
581
692
|
});
|
|
582
693
|
builder.register(RPC, () => ({}), {
|
|
583
694
|
extendProcedureDoc: () => ({
|
|
@@ -627,7 +738,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
627
738
|
const SSERPC = Procedures();
|
|
628
739
|
const TextRPC = Procedures();
|
|
629
740
|
SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
|
|
630
|
-
yield { mode: 'sse' };
|
|
741
|
+
yield { data: { mode: 'sse' } };
|
|
631
742
|
});
|
|
632
743
|
TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
|
|
633
744
|
yield { mode: 'text' };
|
|
@@ -678,7 +789,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
678
789
|
const RPC = Procedures();
|
|
679
790
|
RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
|
|
680
791
|
receivedIsPrevalidated = ctx.isPrevalidated;
|
|
681
|
-
yield { ok: true };
|
|
792
|
+
yield { data: { ok: true } };
|
|
682
793
|
});
|
|
683
794
|
builder.register(RPC, () => ({}));
|
|
684
795
|
const app = builder.build();
|
|
@@ -738,6 +849,77 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
738
849
|
});
|
|
739
850
|
});
|
|
740
851
|
// --------------------------------------------------------------------------
|
|
852
|
+
// SSE Yield Shape Tests
|
|
853
|
+
// --------------------------------------------------------------------------
|
|
854
|
+
describe('SSE yield shape', () => {
|
|
855
|
+
test('custom event names in yields', async () => {
|
|
856
|
+
const builder = new HonoStreamAppBuilder();
|
|
857
|
+
const RPC = Procedures();
|
|
858
|
+
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
859
|
+
yield { data: { type: 'user_joined' }, event: 'join' };
|
|
860
|
+
yield { data: { type: 'message' }, event: 'chat' };
|
|
861
|
+
});
|
|
862
|
+
builder.register(RPC, () => ({}));
|
|
863
|
+
const app = builder.build();
|
|
864
|
+
const res = await app.request('/events/events/1');
|
|
865
|
+
const text = await res.text();
|
|
866
|
+
expect(text).toContain('event: join');
|
|
867
|
+
expect(text).toContain('event: chat');
|
|
868
|
+
expect(text).not.toContain('event: Events');
|
|
869
|
+
});
|
|
870
|
+
test('custom id in yields', async () => {
|
|
871
|
+
const builder = new HonoStreamAppBuilder();
|
|
872
|
+
const RPC = Procedures();
|
|
873
|
+
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
874
|
+
yield { data: { msg: 'first' }, id: 'msg-001' };
|
|
875
|
+
yield { data: { msg: 'second' }, id: 'msg-002' };
|
|
876
|
+
});
|
|
877
|
+
builder.register(RPC, () => ({}));
|
|
878
|
+
const app = builder.build();
|
|
879
|
+
const res = await app.request('/events/events/1');
|
|
880
|
+
const text = await res.text();
|
|
881
|
+
expect(text).toContain('id: msg-001');
|
|
882
|
+
expect(text).toContain('id: msg-002');
|
|
883
|
+
});
|
|
884
|
+
test('string data pass-through without double-stringify', async () => {
|
|
885
|
+
const builder = new HonoStreamAppBuilder();
|
|
886
|
+
const RPC = Procedures();
|
|
887
|
+
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
888
|
+
yield { data: 'already a string' };
|
|
889
|
+
yield { data: { needs: 'stringify' } };
|
|
890
|
+
});
|
|
891
|
+
builder.register(RPC, () => ({}));
|
|
892
|
+
const app = builder.build();
|
|
893
|
+
const res = await app.request('/events/events/1');
|
|
894
|
+
const text = await res.text();
|
|
895
|
+
// String data should be passed through as-is (not JSON-stringified again)
|
|
896
|
+
expect(text).toContain('data: already a string');
|
|
897
|
+
// Object data should be JSON-stringified
|
|
898
|
+
expect(text).toContain('data: {"needs":"stringify"}');
|
|
899
|
+
});
|
|
900
|
+
test('default event falls back to procedure name when omitted', async () => {
|
|
901
|
+
const builder = new HonoStreamAppBuilder();
|
|
902
|
+
const RPC = Procedures();
|
|
903
|
+
RPC.CreateStream('MyProcedure', { scope: 'test', version: 1 }, async function* () {
|
|
904
|
+
yield { data: { value: 1 } };
|
|
905
|
+
yield { data: { value: 2 }, event: 'custom' };
|
|
906
|
+
yield { data: { value: 3 } };
|
|
907
|
+
});
|
|
908
|
+
builder.register(RPC, () => ({}));
|
|
909
|
+
const app = builder.build();
|
|
910
|
+
const res = await app.request('/test/my-procedure/1');
|
|
911
|
+
const text = await res.text();
|
|
912
|
+
// Split into individual SSE messages
|
|
913
|
+
const messages = text.split('\n\n').filter(Boolean);
|
|
914
|
+
// First and third should use procedure name as event
|
|
915
|
+
expect(messages[0]).toContain('event: MyProcedure');
|
|
916
|
+
// Second should use custom event
|
|
917
|
+
expect(messages[1]).toContain('event: custom');
|
|
918
|
+
// Third should fall back to procedure name
|
|
919
|
+
expect(messages[2]).toContain('event: MyProcedure');
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
// --------------------------------------------------------------------------
|
|
741
923
|
// Integration Test
|
|
742
924
|
// --------------------------------------------------------------------------
|
|
743
925
|
describe('integration', () => {
|