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.
- package/README.md +222 -2
- package/build/errors.d.ts +19 -3
- package/build/errors.js +54 -5
- package/build/errors.js.map +1 -1
- package/build/errors.test.js +82 -0
- package/build/errors.test.js.map +1 -1
- package/build/exports.d.ts +1 -0
- package/build/exports.js +1 -0
- package/build/exports.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +92 -0
- package/build/implementations/http/hono-stream/index.js +229 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +681 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +24 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/types.d.ts +15 -1
- package/build/index.d.ts +62 -3
- package/build/index.js +111 -6
- package/build/index.js.map +1 -1
- package/build/index.test.js +385 -2
- package/build/index.test.js.map +1 -1
- package/build/schema/compute-schema.d.ts +9 -2
- package/build/schema/compute-schema.js +9 -3
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -0
- package/build/schema/parser.js +42 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/types.d.ts +1 -0
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/package.json +1 -1
- package/src/errors.test.ts +110 -0
- package/src/errors.ts +65 -3
- package/src/exports.ts +1 -0
- package/src/implementations/http/README.md +87 -55
- package/src/implementations/http/hono-stream/README.md +261 -0
- package/src/implementations/http/hono-stream/index.test.ts +1009 -0
- package/src/implementations/http/hono-stream/index.ts +327 -0
- package/src/implementations/http/hono-stream/types.ts +29 -0
- package/src/implementations/types.ts +17 -1
- package/src/index.test.ts +525 -41
- package/src/index.ts +210 -8
- package/src/schema/compute-schema.ts +15 -3
- package/src/schema/parser.ts +55 -4
- package/src/schema/types.ts +4 -0
- package/src/stack-utils.test.ts +94 -0
- 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
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
361
|
-
)
|
|
362
|
-
await expect(
|
|
363
|
-
|
|
364
|
-
)
|
|
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
|
+
})
|