ts-procedures 3.1.0 → 3.3.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.
Files changed (54) hide show
  1. package/README.md +222 -2
  2. package/build/errors.d.ts +19 -3
  3. package/build/errors.js +54 -5
  4. package/build/errors.js.map +1 -1
  5. package/build/errors.test.js +82 -0
  6. package/build/errors.test.js.map +1 -1
  7. package/build/exports.d.ts +1 -0
  8. package/build/exports.js +1 -0
  9. package/build/exports.js.map +1 -1
  10. package/build/implementations/http/hono-stream/index.d.ts +92 -0
  11. package/build/implementations/http/hono-stream/index.js +229 -0
  12. package/build/implementations/http/hono-stream/index.js.map +1 -0
  13. package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
  14. package/build/implementations/http/hono-stream/index.test.js +681 -0
  15. package/build/implementations/http/hono-stream/index.test.js.map +1 -0
  16. package/build/implementations/http/hono-stream/types.d.ts +24 -0
  17. package/build/implementations/http/hono-stream/types.js +2 -0
  18. package/build/implementations/http/hono-stream/types.js.map +1 -0
  19. package/build/implementations/types.d.ts +15 -1
  20. package/build/index.d.ts +62 -3
  21. package/build/index.js +111 -6
  22. package/build/index.js.map +1 -1
  23. package/build/index.test.js +385 -2
  24. package/build/index.test.js.map +1 -1
  25. package/build/schema/compute-schema.d.ts +9 -2
  26. package/build/schema/compute-schema.js +9 -3
  27. package/build/schema/compute-schema.js.map +1 -1
  28. package/build/schema/parser.d.ts +6 -0
  29. package/build/schema/parser.js +42 -0
  30. package/build/schema/parser.js.map +1 -1
  31. package/build/schema/types.d.ts +1 -0
  32. package/build/stack-utils.d.ts +25 -0
  33. package/build/stack-utils.js +95 -0
  34. package/build/stack-utils.js.map +1 -0
  35. package/build/stack-utils.test.d.ts +1 -0
  36. package/build/stack-utils.test.js +80 -0
  37. package/build/stack-utils.test.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/errors.test.ts +110 -0
  40. package/src/errors.ts +65 -3
  41. package/src/exports.ts +1 -0
  42. package/src/implementations/http/README.md +87 -55
  43. package/src/implementations/http/hono-stream/README.md +261 -0
  44. package/src/implementations/http/hono-stream/index.test.ts +1009 -0
  45. package/src/implementations/http/hono-stream/index.ts +327 -0
  46. package/src/implementations/http/hono-stream/types.ts +29 -0
  47. package/src/implementations/types.ts +17 -1
  48. package/src/index.test.ts +525 -41
  49. package/src/index.ts +210 -8
  50. package/src/schema/compute-schema.ts +15 -3
  51. package/src/schema/parser.ts +55 -4
  52. package/src/schema/types.ts +4 -0
  53. package/src/stack-utils.test.ts +94 -0
  54. package/src/stack-utils.ts +129 -0
package/src/index.test.ts CHANGED
@@ -3,10 +3,7 @@ import { describe, expect, test } from 'vitest'
3
3
  import { Procedures } from './index.js'
4
4
  import { v } from 'suretype'
5
5
  import { Type } from 'typebox'
6
- import {
7
- ProcedureError,
8
- ProcedureValidationError,
9
- } from './errors.js'
6
+ import { ProcedureError, ProcedureValidationError } from './errors.js'
10
7
 
11
8
  describe('Procedures', () => {
12
9
  test('Procedures', () => {
@@ -31,7 +28,7 @@ describe('Procedures', () => {
31
28
 
32
29
  const { Create } = Procedures<CustomContext, ExtendedConfig>()
33
30
 
34
- const {info} = Create(
31
+ const { info } = Create(
35
32
  'TestProcedure',
36
33
  {
37
34
  // should not throw type errors
@@ -40,7 +37,7 @@ describe('Procedures', () => {
40
37
  async (ctx) => {
41
38
  // should not throw type errors
42
39
  return ctx.authToken
43
- },
40
+ }
44
41
  )
45
42
 
46
43
  expect(info.customProp).toEqual('customProp')
@@ -48,20 +45,12 @@ describe('Procedures', () => {
48
45
  })
49
46
 
50
47
  test('Create Single Procedures', () => {
51
- const { procedure: procedure1, info: info1 } = Procedures().Create(
52
- 'test1',
53
- {},
54
- async () => {
55
- return '1'
56
- },
57
- )
58
- const { procedure: procedure2, info: info2 } = Procedures().Create(
59
- 'test2',
60
- {},
61
- async () => {
62
- return '2'
63
- },
64
- )
48
+ const { procedure: procedure1, info: info1 } = Procedures().Create('test1', {}, async () => {
49
+ return '1'
50
+ })
51
+ const { procedure: procedure2, info: info2 } = Procedures().Create('test2', {}, async () => {
52
+ return '2'
53
+ })
65
54
 
66
55
  expect(procedure1).toBeDefined()
67
56
  expect(info1).toBeDefined()
@@ -91,7 +80,7 @@ describe('Procedures', () => {
91
80
  expect(params).toEqual({ name: 'name' })
92
81
  done()
93
82
  return 'name'
94
- },
83
+ }
95
84
  )
96
85
 
97
86
  mockHttpCall({}, { name: 'name' })
@@ -119,7 +108,7 @@ describe('Procedures', () => {
119
108
  expect(params).toEqual({ name: 'name' })
120
109
  done()
121
110
  return 'name'
122
- },
111
+ }
123
112
  )
124
113
 
125
114
  mockHttpCall({}, { name: 'name' })
@@ -144,7 +133,7 @@ describe('Procedures', () => {
144
133
  },
145
134
  async (ctx, params) => {
146
135
  return params.number
147
- },
136
+ }
148
137
  )
149
138
 
150
139
  expect(NamedExportHandler).toBeDefined()
@@ -200,7 +189,7 @@ describe('Procedures', () => {
200
189
  },
201
190
  async () => {
202
191
  done()
203
- },
192
+ }
204
193
  )
205
194
 
206
195
  mockHttpCall()
@@ -243,7 +232,7 @@ describe('Procedures', () => {
243
232
  },
244
233
  async () => {
245
234
  return
246
- },
235
+ }
247
236
  )
248
237
 
249
238
  mockHttpCall({})
@@ -272,13 +261,9 @@ describe('Procedures', () => {
272
261
  test('Procedure handler can throw local ctx error and is caught', async () => {
273
262
  const { Create } = Procedures()
274
263
 
275
- const { TestProcedureHandlerError } = Create(
276
- 'TestProcedureHandlerError',
277
- {},
278
- async (ctx) => {
279
- throw ctx.error( 'Local context error')
280
- },
281
- )
264
+ const { TestProcedureHandlerError } = Create('TestProcedureHandlerError', {}, async (ctx) => {
265
+ throw ctx.error('Local context error')
266
+ })
282
267
 
283
268
  try {
284
269
  await TestProcedureHandlerError({}, {})
@@ -307,11 +292,11 @@ describe('Procedures', () => {
307
292
  },
308
293
  async () => {
309
294
  return 'test-docs'
310
- },
295
+ }
311
296
  )
312
297
 
313
298
  const procedures = getProcedures()
314
- const testDocsProcedure = procedures.find(p => p.name === 'test-docs')
299
+ const testDocsProcedure = procedures.find((p) => p.name === 'test-docs')
315
300
  expect(testDocsProcedure).toBeDefined()
316
301
  expect(testDocsProcedure?.config?.schema).toEqual({
317
302
  params: {
@@ -353,15 +338,15 @@ describe('Procedures', () => {
353
338
  }
354
339
 
355
340
  return 'User authentication is valid'
356
- },
341
+ }
357
342
  )
358
343
 
359
- await expect(
360
- CheckIsAuthenticated({ authToken: 'valid-token' }, {}),
361
- ).resolves.toEqual('User authentication is valid')
362
- await expect(
363
- CheckIsAuthenticated({ authToken: 'not-valid-token' }, {}),
364
- ).rejects.toThrowError(ProcedureError)
344
+ await expect(CheckIsAuthenticated({ authToken: 'valid-token' }, {})).resolves.toEqual(
345
+ 'User authentication is valid'
346
+ )
347
+ await expect(CheckIsAuthenticated({ authToken: 'not-valid-token' }, {})).rejects.toThrowError(
348
+ ProcedureError
349
+ )
365
350
  })
366
351
 
367
352
  test('Procedures - duplicate registration throws before schema computation', () => {
@@ -447,3 +432,502 @@ describe('Procedures', () => {
447
432
  }
448
433
  })
449
434
  })
435
+
436
+ describe('Procedures - Definition Location in Errors', () => {
437
+ test('ProcedureValidationError includes definition location', async () => {
438
+ const { Create } = Procedures()
439
+
440
+ const { TestValidation } = Create(
441
+ 'TestValidation',
442
+ {
443
+ schema: {
444
+ params: v.object({ name: v.string().required() }),
445
+ },
446
+ },
447
+ async (ctx, params) => {
448
+ return params.name
449
+ }
450
+ )
451
+
452
+ try {
453
+ // @ts-expect-error - intentionally passing invalid params
454
+ await TestValidation({}, {}) // Missing required 'name' param
455
+ } catch (e: any) {
456
+ expect(e).toBeInstanceOf(ProcedureValidationError)
457
+ expect(e.definedAt).toBeDefined()
458
+ expect(e.definedAt.file).toContain('index.test.ts')
459
+ expect(e.definedAt.line).toBeGreaterThan(0)
460
+ expect(e.definedAt.column).toBeGreaterThan(0)
461
+ expect(e.stack).toContain('--- Procedure "TestValidation" defined at ---')
462
+ }
463
+ })
464
+
465
+ test('ctx.error() includes definition location', async () => {
466
+ const { Create } = Procedures()
467
+
468
+ const { TestCtxError } = Create('TestCtxError', {}, async (ctx) => {
469
+ throw ctx.error('Custom error')
470
+ })
471
+
472
+ try {
473
+ await TestCtxError({}, {})
474
+ } catch (e: any) {
475
+ expect(e).toBeInstanceOf(ProcedureError)
476
+ expect(e.definedAt).toBeDefined()
477
+ expect(e.definedAt.file).toContain('index.test.ts')
478
+ expect(e.getDefinitionLocation()).toBeDefined()
479
+ expect(e.stack).toContain('--- Procedure "TestCtxError" defined at ---')
480
+ }
481
+ })
482
+
483
+ test('wrapped errors include definition location', async () => {
484
+ const { Create } = Procedures()
485
+
486
+ const { TestWrappedError } = Create('TestWrappedError', {}, async () => {
487
+ throw new Error('Original error')
488
+ })
489
+
490
+ try {
491
+ await TestWrappedError({}, {})
492
+ } catch (e: any) {
493
+ expect(e).toBeInstanceOf(ProcedureError)
494
+ expect(e.definedAt).toBeDefined()
495
+ expect(e.definedAt.file).toContain('index.test.ts')
496
+ expect(e.message).toContain('Error in handler for TestWrappedError')
497
+ expect(e.cause).toBeInstanceOf(Error)
498
+ expect(e.cause.message).toBe('Original error')
499
+ }
500
+ })
501
+
502
+ test('getDefinitionLocation returns formatted string', async () => {
503
+ const { Create } = Procedures()
504
+
505
+ const { TestGetLocation } = Create(
506
+ 'TestGetLocation',
507
+ {
508
+ schema: {
509
+ params: v.object({ id: v.number().required() }),
510
+ },
511
+ },
512
+ async (ctx, params) => {
513
+ return params.id
514
+ }
515
+ )
516
+
517
+ try {
518
+ // @ts-expect-error - intentionally passing invalid params
519
+ await TestGetLocation({}, {}) // Missing required 'id' param
520
+ } catch (e: any) {
521
+ const location = e.getDefinitionLocation()
522
+ expect(location).toBeDefined()
523
+ expect(location).toMatch(/index\.test\.ts:\d+:\d+/)
524
+ }
525
+ })
526
+
527
+ test('error stack shows procedure definition location at the end', async () => {
528
+ const { Create } = Procedures()
529
+
530
+ const { TestStackFormat } = Create(
531
+ 'TestStackFormat',
532
+ {
533
+ schema: {
534
+ params: v.object({ value: v.string().required() }),
535
+ },
536
+ },
537
+ async (ctx, params) => {
538
+ return params.value
539
+ }
540
+ )
541
+
542
+ try {
543
+ // @ts-expect-error - intentionally passing invalid params
544
+ await TestStackFormat({}, {})
545
+ } catch (e: any) {
546
+ // Verify it's a validation error
547
+ expect(e.name).toBe('ProcedureValidationError')
548
+ expect(e).toBeInstanceOf(ProcedureValidationError)
549
+ // Stack should contain the error message and definition location
550
+ expect(e.stack).toContain('Validation error for TestStackFormat')
551
+ expect(e.stack).toContain('--- Procedure "TestStackFormat" defined at ---')
552
+ // The definition section should be at the end of the stack
553
+ const stackLines = e.stack.split('\n')
554
+ const definitionIndex = stackLines.findIndex((line: string) =>
555
+ line.includes('--- Procedure "TestStackFormat" defined at ---')
556
+ )
557
+ expect(definitionIndex).toBeGreaterThan(0)
558
+ }
559
+ })
560
+ })
561
+
562
+ describe('Streaming Procedures - CreateStream', () => {
563
+ test('CreateStream creates a streaming procedure', async () => {
564
+ const { CreateStream } = Procedures()
565
+
566
+ const { StreamTest, procedure, info } = CreateStream(
567
+ 'StreamTest',
568
+ {
569
+ description: 'Test streaming procedure',
570
+ schema: {
571
+ params: v.object({ count: v.number().required() }),
572
+ yieldType: v.object({ value: v.number().required() }),
573
+ },
574
+ },
575
+ async function* (ctx, params) {
576
+ for (let i = 0; i < params.count; i++) {
577
+ yield { value: i }
578
+ }
579
+ }
580
+ )
581
+
582
+ expect(StreamTest).toBeDefined()
583
+ expect(procedure).toBeDefined()
584
+ expect(info.isStream).toBe(true)
585
+ expect(info.name).toBe('StreamTest')
586
+ expect(info.description).toBe('Test streaming procedure')
587
+ expect(info.schema.params).toEqual({
588
+ type: 'object',
589
+ properties: { count: { type: 'number' } },
590
+ required: ['count'],
591
+ })
592
+ expect(info.schema.yieldType).toEqual({
593
+ type: 'object',
594
+ properties: { value: { type: 'number' } },
595
+ required: ['value'],
596
+ })
597
+
598
+ // Collect yielded values
599
+ const values: { value: number }[] = []
600
+ for await (const val of StreamTest({}, { count: 3 })) {
601
+ values.push(val)
602
+ }
603
+
604
+ expect(values).toEqual([{ value: 0 }, { value: 1 }, { value: 2 }])
605
+ })
606
+
607
+ test('CreateStream validates params', async () => {
608
+ const { CreateStream } = Procedures()
609
+
610
+ const { StreamWithParams } = CreateStream(
611
+ 'StreamWithParams',
612
+ {
613
+ schema: {
614
+ params: v.object({ name: v.string().required() }),
615
+ yieldType: v.string(),
616
+ },
617
+ },
618
+ async function* (ctx, params) {
619
+ yield params.name
620
+ }
621
+ )
622
+
623
+ // Missing required param should throw ProcedureValidationError
624
+ try {
625
+ // @ts-expect-error - intentionally passing invalid params
626
+ for await (const _val of StreamWithParams({}, {})) {
627
+ // Should not reach here
628
+ }
629
+ expect.fail('Should have thrown')
630
+ } catch (e: any) {
631
+ expect(e).toBeInstanceOf(ProcedureValidationError)
632
+ expect(e.message).toContain('Validation error for StreamWithParams')
633
+ }
634
+ })
635
+
636
+ test('CreateStream with validateYields validates each yielded value', async () => {
637
+ const { CreateStream } = Procedures()
638
+ const { ProcedureYieldValidationError } = await import('./errors.js')
639
+
640
+ const { StreamValidateYields } = CreateStream(
641
+ 'StreamValidateYields',
642
+ {
643
+ schema: {
644
+ yieldType: v.object({ id: v.number().required() }),
645
+ },
646
+ validateYields: true,
647
+ },
648
+ async function* () {
649
+ yield { id: 1 } // Valid
650
+ yield { id: 'not-a-number' } as any // Invalid - should throw
651
+ }
652
+ )
653
+
654
+ const values: any[] = []
655
+ try {
656
+ for await (const val of StreamValidateYields({}, {})) {
657
+ values.push(val)
658
+ }
659
+ expect.fail('Should have thrown on invalid yield')
660
+ } catch (e: any) {
661
+ expect(e).toBeInstanceOf(ProcedureYieldValidationError)
662
+ expect(e.message).toContain('Yield validation error for StreamValidateYields')
663
+ expect(values).toEqual([{ id: 1 }]) // First value was valid
664
+ }
665
+ })
666
+
667
+ test('CreateStream without validateYields does not validate yields', async () => {
668
+ const { CreateStream } = Procedures()
669
+
670
+ const { StreamNoValidate } = CreateStream(
671
+ 'StreamNoValidate',
672
+ {
673
+ schema: {
674
+ yieldType: v.object({ id: v.number().required() }),
675
+ },
676
+ // validateYields defaults to false
677
+ },
678
+ async function* () {
679
+ yield { id: 1 }
680
+ yield { id: 'not-a-number' } as any // Invalid but won't throw
681
+ }
682
+ )
683
+
684
+ const values: any[] = []
685
+ for await (const val of StreamNoValidate({}, {})) {
686
+ values.push(val)
687
+ }
688
+
689
+ expect(values).toEqual([{ id: 1 }, { id: 'not-a-number' }])
690
+ })
691
+
692
+ test('CreateStream ctx.error throws ProcedureError', async () => {
693
+ const { CreateStream } = Procedures()
694
+
695
+ const { StreamError } = CreateStream('StreamError', {}, async function* (ctx) {
696
+ yield 'first'
697
+ throw ctx.error('Custom stream error', { code: 'STREAM_ERR' })
698
+ })
699
+
700
+ const values: any[] = []
701
+ try {
702
+ for await (const val of StreamError({}, {})) {
703
+ values.push(val)
704
+ }
705
+ expect.fail('Should have thrown')
706
+ } catch (e: any) {
707
+ expect(e).toBeInstanceOf(ProcedureError)
708
+ expect(e.message).toBe('Custom stream error')
709
+ expect(e.procedureName).toBe('StreamError')
710
+ expect(e.meta).toEqual({ code: 'STREAM_ERR' })
711
+ expect(values).toEqual(['first'])
712
+ }
713
+ })
714
+
715
+ test('CreateStream provides ctx.signal for abort handling', async () => {
716
+ const { CreateStream } = Procedures()
717
+ let signalReceived = false
718
+
719
+ const { StreamWithSignal } = CreateStream('StreamWithSignal', {}, async function* (ctx) {
720
+ expect(ctx.signal).toBeDefined()
721
+ expect(ctx.signal).toBeInstanceOf(AbortSignal)
722
+ signalReceived = true
723
+ yield 'value'
724
+ })
725
+
726
+ for await (const _val of StreamWithSignal({}, {})) {
727
+ // consume
728
+ }
729
+
730
+ expect(signalReceived).toBe(true)
731
+ })
732
+
733
+ test('CreateStream appears in getProcedures with isStream flag', () => {
734
+ const { Create, CreateStream, getProcedures } = Procedures()
735
+
736
+ Create('RegularProc', {}, async () => 'regular')
737
+
738
+ CreateStream('StreamProc', {}, async function* () {
739
+ yield 'stream'
740
+ })
741
+
742
+ const procs = getProcedures()
743
+ expect(procs.length).toBe(2)
744
+
745
+ const regular = procs.find((p) => p.name === 'RegularProc')
746
+ const stream = procs.find((p) => p.name === 'StreamProc')
747
+
748
+ expect(regular?.isStream).toBeUndefined()
749
+ expect(stream?.isStream).toBe(true)
750
+ })
751
+
752
+ test('CreateStream onCreate callback receives isStream flag', () =>
753
+ new Promise<void>((done) => {
754
+ let receivedProc: any
755
+
756
+ const { CreateStream } = Procedures({
757
+ onCreate: (proc) => {
758
+ receivedProc = proc
759
+ },
760
+ })
761
+
762
+ CreateStream('OnCreateStream', {}, async function* () {
763
+ yield 'test'
764
+ })
765
+
766
+ expect(receivedProc).toBeDefined()
767
+ expect(receivedProc.isStream).toBe(true)
768
+ expect(receivedProc.name).toBe('OnCreateStream')
769
+ done()
770
+ }))
771
+
772
+ test('CreateStream duplicate registration throws', () => {
773
+ const { CreateStream } = Procedures()
774
+
775
+ CreateStream('DuplicateStream', {}, async function* () {
776
+ yield 'first'
777
+ })
778
+
779
+ expect(() => {
780
+ CreateStream('DuplicateStream', {}, async function* () {
781
+ yield 'second'
782
+ })
783
+ }).toThrow('Procedure with name DuplicateStream is already registered')
784
+ })
785
+
786
+ test('CreateStream error includes definition location', async () => {
787
+ const { CreateStream } = Procedures()
788
+
789
+ const { StreamErrorLocation } = CreateStream(
790
+ 'StreamErrorLocation',
791
+ {
792
+ schema: {
793
+ params: v.object({ id: v.number().required() }),
794
+ },
795
+ },
796
+ async function* () {
797
+ yield 'test'
798
+ }
799
+ )
800
+
801
+ try {
802
+ // @ts-expect-error - intentionally passing invalid params
803
+ for await (const _val of StreamErrorLocation({}, {})) {
804
+ // consume
805
+ }
806
+ expect.fail('Should have thrown')
807
+ } catch (e: any) {
808
+ expect(e.definedAt).toBeDefined()
809
+ expect(e.definedAt.file).toContain('index.test.ts')
810
+ expect(e.stack).toContain('--- Procedure "StreamErrorLocation" defined at ---')
811
+ }
812
+ })
813
+
814
+ test('CreateStream with Typebox schema', async () => {
815
+ const { CreateStream } = Procedures()
816
+
817
+ const { TypeboxStream } = CreateStream(
818
+ 'TypeboxStream',
819
+ {
820
+ schema: {
821
+ params: Type.Object({ limit: Type.Number() }),
822
+ yieldType: Type.Object({ data: Type.String() }),
823
+ },
824
+ },
825
+ async function* (ctx, params) {
826
+ for (let i = 0; i < params.limit; i++) {
827
+ yield { data: `item-${i}` }
828
+ }
829
+ }
830
+ )
831
+
832
+ const values: any[] = []
833
+ for await (const val of TypeboxStream({}, { limit: 2 })) {
834
+ values.push(val)
835
+ }
836
+
837
+ expect(values).toEqual([{ data: 'item-0' }, { data: 'item-1' }])
838
+ })
839
+
840
+ test('CreateStream with context type', async () => {
841
+ interface StreamContext {
842
+ userId: string
843
+ }
844
+
845
+ const { CreateStream } = Procedures<StreamContext>()
846
+
847
+ const { ContextStream } = CreateStream('ContextStream', {}, async function* (ctx) {
848
+ // ctx should have both userId and error
849
+ expect(ctx.userId).toBe('user-123')
850
+ expect(ctx.error).toBeDefined()
851
+ expect(ctx.signal).toBeDefined()
852
+ yield ctx.userId
853
+ })
854
+
855
+ const values: any[] = []
856
+ for await (const val of ContextStream({ userId: 'user-123' }, {})) {
857
+ values.push(val)
858
+ }
859
+
860
+ expect(values).toEqual(['user-123'])
861
+ })
862
+
863
+ test('CreateStream wrapped errors preserve cause', async () => {
864
+ const { CreateStream } = Procedures()
865
+ const originalError = new Error('Stream underlying error')
866
+ ;(originalError as any).code = 'STREAM_FAIL'
867
+
868
+ const { StreamCause } = CreateStream(
869
+ 'StreamCause',
870
+ {},
871
+ // eslint-disable-next-line require-yield
872
+ async function* () {
873
+ throw originalError
874
+ }
875
+ )
876
+
877
+ try {
878
+ for await (const _val of StreamCause({}, {})) {
879
+ // consume
880
+ }
881
+ expect.fail('Should have thrown')
882
+ } catch (e: any) {
883
+ expect(e).toBeInstanceOf(ProcedureError)
884
+ expect(e.cause).toBe(originalError)
885
+ expect(e.cause.code).toBe('STREAM_FAIL')
886
+ }
887
+ })
888
+
889
+ test('CreateStream with extended config', () => {
890
+ interface ExtConfig {
891
+ scope: string
892
+ version: number
893
+ }
894
+
895
+ const { CreateStream } = Procedures<unknown, ExtConfig>()
896
+
897
+ const { info } = CreateStream(
898
+ 'ExtendedStream',
899
+ {
900
+ scope: 'api',
901
+ version: 1,
902
+ description: 'Extended config stream',
903
+ },
904
+ async function* () {
905
+ yield 'data'
906
+ }
907
+ )
908
+
909
+ expect(info.scope).toBe('api')
910
+ expect(info.version).toBe(1)
911
+ expect(info.description).toBe('Extended config stream')
912
+ expect(info.isStream).toBe(true)
913
+ })
914
+
915
+ test('CreateStream signal.aborted becomes true after generator completes', async () => {
916
+ const { CreateStream } = Procedures()
917
+ let capturedSignal: AbortSignal | null = null
918
+
919
+ const { AbortStream } = CreateStream('AbortStream', {}, async function* (ctx) {
920
+ capturedSignal = ctx.signal
921
+ yield 'value'
922
+ })
923
+
924
+ // Consume the generator
925
+ for await (const _val of AbortStream({}, {})) {
926
+ // consume
927
+ }
928
+
929
+ // After generator completes, signal should be aborted
930
+ expect(capturedSignal).not.toBeNull()
931
+ expect(capturedSignal!.aborted).toBe(true)
932
+ })
933
+ })