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.
@@ -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({ onStreamError: errorHandler });
345
+ const builder = new HonoStreamAppBuilder({ onPreStreamError: errorHandler });
343
346
  const RPC = Procedures();
344
- // Error during context resolution (before streaming starts)
345
- builder.register(RPC, () => {
346
- throw new Error('Context error');
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
- // Since no streaming procedures were registered, this should 404
350
- // Let's test with a real streaming procedure that throws during setup
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('onStreamError is called when params fail validation', async () => {
384
- const onStreamError = vi.fn((procedure, c, error) => {
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
- // HTTP error response returned (not a stream)
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
- test('validation errors return 400 by default when no onStreamError handler', async () => {
411
- const builder = new HonoStreamAppBuilder();
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
- // Default: returns 400 JSON error
426
- expect(res.status).toBe(400);
436
+ expect(res.status).toBe(422);
427
437
  const body = await res.json();
428
- expect(body.error).toContain('Validation error');
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', () => {