ts-procedures 5.4.0 → 5.4.1

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 (34) hide show
  1. package/package.json +1 -2
  2. package/src/errors.test.ts +0 -163
  3. package/src/errors.ts +0 -107
  4. package/src/exports.ts +0 -7
  5. package/src/implementations/http/README.md +0 -260
  6. package/src/implementations/http/express-rpc/README.md +0 -281
  7. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  8. package/src/implementations/http/express-rpc/index.ts +0 -265
  9. package/src/implementations/http/express-rpc/types.ts +0 -16
  10. package/src/implementations/http/hono-api/index.test.ts +0 -1328
  11. package/src/implementations/http/hono-api/index.ts +0 -461
  12. package/src/implementations/http/hono-api/types.ts +0 -16
  13. package/src/implementations/http/hono-rpc/README.md +0 -358
  14. package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
  15. package/src/implementations/http/hono-rpc/index.ts +0 -237
  16. package/src/implementations/http/hono-rpc/types.ts +0 -16
  17. package/src/implementations/http/hono-stream/README.md +0 -526
  18. package/src/implementations/http/hono-stream/index.test.ts +0 -1676
  19. package/src/implementations/http/hono-stream/index.ts +0 -435
  20. package/src/implementations/http/hono-stream/types.ts +0 -29
  21. package/src/implementations/types.ts +0 -127
  22. package/src/index.test.ts +0 -1194
  23. package/src/index.ts +0 -512
  24. package/src/schema/compute-schema.test.ts +0 -128
  25. package/src/schema/compute-schema.ts +0 -88
  26. package/src/schema/extract-json-schema.test.ts +0 -25
  27. package/src/schema/extract-json-schema.ts +0 -15
  28. package/src/schema/parser.test.ts +0 -182
  29. package/src/schema/parser.ts +0 -215
  30. package/src/schema/resolve-schema-lib.test.ts +0 -19
  31. package/src/schema/resolve-schema-lib.ts +0 -29
  32. package/src/schema/types.ts +0 -20
  33. package/src/stack-utils.test.ts +0 -94
  34. package/src/stack-utils.ts +0 -129
package/src/index.test.ts DELETED
@@ -1,1194 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import { describe, expect, test } from 'vitest'
3
- import { Procedures } from './index.js'
4
- import { v } from 'suretype'
5
- import { Type } from 'typebox'
6
- import { ProcedureError, ProcedureValidationError } from './errors.js'
7
-
8
- describe('Procedures', () => {
9
- test('Procedures', () => {
10
- const result = Procedures({
11
- onCreate: () => {
12
- return undefined
13
- },
14
- })
15
-
16
- expect(result).toHaveProperty('Create')
17
- })
18
-
19
- test('Procedures generic context & extended config', () => {
20
- interface CustomContext {
21
- authToken: string
22
- }
23
-
24
- interface ExtendedConfig {
25
- customProp: string
26
- optionalProp?: number
27
- }
28
-
29
- const { Create } = Procedures<CustomContext, ExtendedConfig>()
30
-
31
- const { info } = Create(
32
- 'TestProcedure',
33
- {
34
- // should not throw type errors
35
- customProp: 'customProp',
36
- },
37
- async (ctx) => {
38
- // should not throw type errors
39
- return ctx.authToken
40
- }
41
- )
42
-
43
- expect(info.customProp).toEqual('customProp')
44
- expect(info.optionalProp).toEqual(undefined)
45
- })
46
-
47
- test('Create Single Procedures', () => {
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
- })
54
-
55
- expect(procedure1).toBeDefined()
56
- expect(info1).toBeDefined()
57
- expect(procedure2).toBeDefined()
58
- expect(info2).toBeDefined()
59
- })
60
-
61
- test('Procedures - Create call', () =>
62
- new Promise<void>((done) => {
63
- let mockHttpCall: any
64
-
65
- const { Create } = Procedures({
66
- onCreate: ({ handler }) => {
67
- mockHttpCall = handler
68
- },
69
- })
70
-
71
- Create(
72
- 'Handler',
73
- {
74
- schema: {
75
- params: v.object({ name: v.string() }),
76
- returnType: v.string(),
77
- },
78
- },
79
- async (ctx, params) => {
80
- expect(params).toEqual({ name: 'name' })
81
- done()
82
- return 'name'
83
- }
84
- )
85
-
86
- mockHttpCall({}, { name: 'name' })
87
- }))
88
-
89
- test('Procedures - Create call w/ Typebox', () =>
90
- new Promise<void>((done) => {
91
- let mockHttpCall: any
92
-
93
- const { Create } = Procedures({
94
- onCreate: ({ handler, config, name }) => {
95
- mockHttpCall = handler
96
- },
97
- })
98
-
99
- Create(
100
- 'Handler',
101
- {
102
- schema: {
103
- params: Type.Object({ name: Type.Optional(Type.String()) }),
104
- returnType: Type.String(),
105
- },
106
- },
107
- async (ctx, params) => {
108
- expect(params).toEqual({ name: 'name' })
109
- done()
110
- return 'name'
111
- }
112
- )
113
-
114
- mockHttpCall({}, { name: 'name' })
115
- }))
116
-
117
- test('Procedures - Create returns a handler to call/test the Procedure registration', async () => {
118
- let ProcedureRegisteredCbHandler: any
119
-
120
- const { Create } = Procedures({
121
- onCreate: (Procedure) => {
122
- ProcedureRegisteredCbHandler = Procedure.handler
123
- },
124
- })
125
-
126
- const { NamedExportHandler, procedure, info } = Create(
127
- 'NamedExportHandler',
128
- {
129
- description: 'Handler description',
130
- schema: {
131
- params: v.object({ number: v.number() }),
132
- },
133
- },
134
- async (ctx, params) => {
135
- return params.number
136
- }
137
- )
138
-
139
- expect(NamedExportHandler).toBeDefined()
140
- expect(procedure).toBeDefined()
141
- expect(ProcedureRegisteredCbHandler).toEqual(NamedExportHandler)
142
- expect(ProcedureRegisteredCbHandler).toEqual(procedure)
143
-
144
- const result = NamedExportHandler({}, { number: 1 })
145
-
146
- expect(result).toBeDefined()
147
- expect(result).toBeInstanceOf(Promise)
148
- await expect(result).resolves.toEqual(1)
149
-
150
- expect(info).toBeDefined()
151
- expect(info).toBeInstanceOf(Object)
152
- expect(info.schema).toHaveProperty('params')
153
- expect(info.schema.params).toEqual({
154
- type: 'object',
155
- properties: { number: { type: 'number' } },
156
- })
157
- expect(info).toHaveProperty('description')
158
- expect(info.description).toEqual('Handler description')
159
- })
160
-
161
- test('Procedures - Create params validation w/ no params provided', () =>
162
- new Promise<void>((done) => {
163
- let mockHttpCall: any
164
-
165
- const { Create } = Procedures({
166
- onCreate: ({ handler, config, name }) => {
167
- mockHttpCall = (callParams: any) => {
168
- if (config.validation?.params) {
169
- const { errors } = config.validation.params(callParams)
170
-
171
- if (errors && 'message' in errors[0]) {
172
- expect(errors[0].message).toEqual('must be object')
173
- done()
174
- return
175
- }
176
- }
177
-
178
- handler(callParams, {})
179
- }
180
- },
181
- })
182
-
183
- Create(
184
- 'test',
185
- {
186
- schema: {
187
- params: v.object({}),
188
- },
189
- },
190
- async () => {
191
- done()
192
- }
193
- )
194
-
195
- mockHttpCall()
196
- }))
197
-
198
- test('Procedures - Create params validation w/ missing params', async () =>
199
- new Promise<void>((done) => {
200
- let mockHttpCall: any
201
-
202
- const { Create } = Procedures({
203
- onCreate: async ({ handler, config, name }) => {
204
- mockHttpCall = async (callParams: any) => {
205
- if (config.validation?.params) {
206
- const { errors } = config.validation.params(callParams)
207
- expect(errors).toBeDefined()
208
- expect(errors?.length).toEqual(2)
209
- }
210
-
211
- try {
212
- await handler(callParams, {})
213
- } catch (e: any) {
214
- expect(e).instanceof(ProcedureValidationError)
215
- expect(e.errors.length).toEqual(2)
216
- done()
217
- }
218
- }
219
- },
220
- })
221
-
222
- Create(
223
- 'test',
224
- {
225
- schema: {
226
- params: v.object({
227
- name: v.string().required(),
228
- id: v.number().required(),
229
- email: v.string(),
230
- }),
231
- },
232
- },
233
- async () => {
234
- return
235
- }
236
- )
237
-
238
- mockHttpCall({})
239
- }))
240
-
241
- test('Procedures - Create call provides ctx to handler', () =>
242
- new Promise<void>((done) => {
243
- let mockHttpCall: any
244
-
245
- const { Create } = Procedures<{
246
- testCtx: string
247
- }>({
248
- onCreate: ({ handler }) => {
249
- mockHttpCall = () => handler({ testCtx: 'testCtx' })
250
- },
251
- })
252
-
253
- Create('test', {}, async (ctx, params) => {
254
- expect(ctx.testCtx).toEqual('testCtx')
255
- done()
256
- })
257
-
258
- mockHttpCall()
259
- }))
260
-
261
- test('Procedure handler can throw local ctx error and is caught', async () => {
262
- const { Create } = Procedures()
263
-
264
- const { TestProcedureHandlerError } = Create('TestProcedureHandlerError', {}, async (ctx) => {
265
- throw ctx.error('Local context error')
266
- })
267
-
268
- try {
269
- await TestProcedureHandlerError({}, {})
270
- } catch (e: any) {
271
- expect(e).toBeInstanceOf(ProcedureError)
272
-
273
- expect(e.message).toEqual('Local context error')
274
- expect(e.procedureName).toEqual('TestProcedureHandlerError')
275
- }
276
- })
277
-
278
- test('Procedures - getRegisteredProcedures', () => {
279
- const { Create, getProcedures } = Procedures({
280
- onCreate: () => {
281
- return undefined
282
- },
283
- })
284
-
285
- Create(
286
- 'test-docs',
287
- {
288
- schema: {
289
- params: v.object({ name: v.string().required() }),
290
- returnType: v.string(),
291
- },
292
- },
293
- async () => {
294
- return 'test-docs'
295
- }
296
- )
297
-
298
- const procedures = getProcedures()
299
- const testDocsProcedure = procedures.find((p) => p.name === 'test-docs')
300
- expect(testDocsProcedure).toBeDefined()
301
- expect(testDocsProcedure?.config?.schema).toEqual({
302
- params: {
303
- type: 'object',
304
- properties: {
305
- name: {
306
- type: 'string',
307
- },
308
- },
309
- required: ['name'],
310
- },
311
- returnType: {
312
- type: 'string',
313
- },
314
- })
315
- })
316
-
317
- test('Procedures - context() throws', async () => {
318
- interface CustomContext {
319
- authToken: string
320
- }
321
-
322
- const { Create } = Procedures<CustomContext>()
323
-
324
- function validateAuthToken(token: string) {
325
- return token === 'valid-token'
326
- }
327
-
328
- const { CheckIsAuthenticated } = Create(
329
- 'CheckIsAuthenticated',
330
- {
331
- schema: {
332
- returnType: v.string(),
333
- },
334
- },
335
- async (ctx) => {
336
- if (!validateAuthToken(ctx.authToken)) {
337
- throw ctx.error('Invalid auth token')
338
- }
339
-
340
- return 'User authentication is valid'
341
- }
342
- )
343
-
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
- )
350
- })
351
-
352
- test('Procedures - duplicate registration throws before schema computation', () => {
353
- const { Create } = Procedures()
354
-
355
- Create('DuplicateTest', {}, async () => 'first')
356
-
357
- expect(() => {
358
- Create('DuplicateTest', {}, async () => 'second')
359
- }).toThrow('Procedure with name DuplicateTest is already registered')
360
- })
361
-
362
- test('Procedures - wrapped errors preserve cause', async () => {
363
- const { Create } = Procedures()
364
- const originalError = new Error('Database connection failed')
365
- ;(originalError as any).code = 'ECONNREFUSED'
366
-
367
- const { TestCause } = Create('TestCause', {}, async () => {
368
- throw originalError
369
- })
370
-
371
- try {
372
- await TestCause({}, {})
373
- } catch (e: any) {
374
- expect(e).toBeInstanceOf(ProcedureError)
375
- expect(e.cause).toBe(originalError)
376
- expect(e.cause.code).toBe('ECONNREFUSED')
377
- }
378
- })
379
-
380
- test('Procedures - getProcedure returns specific procedure', () => {
381
- const { Create, getProcedure } = Procedures()
382
-
383
- Create('FindMe', {}, async () => 'found')
384
-
385
- const proc = getProcedure('FindMe')
386
- expect(proc).toBeDefined()
387
- expect(proc?.name).toBe('FindMe')
388
-
389
- expect(getProcedure('NotFound')).toBeUndefined()
390
- })
391
-
392
- test('Procedures - removeProcedure allows re-registration', () => {
393
- const { Create, removeProcedure, getProcedure } = Procedures()
394
-
395
- Create('Removable', {}, async () => 'v1')
396
- expect(getProcedure('Removable')).toBeDefined()
397
-
398
- const removed = removeProcedure('Removable')
399
- expect(removed).toBe(true)
400
- expect(getProcedure('Removable')).toBeUndefined()
401
-
402
- // Can now re-register
403
- Create('Removable', {}, async () => 'v2')
404
- expect(getProcedure('Removable')).toBeDefined()
405
- })
406
-
407
- test('Procedures - clear removes all procedures', () => {
408
- const { Create, getProcedures, clear } = Procedures()
409
-
410
- Create('One', {}, async () => '1')
411
- Create('Two', {}, async () => '2')
412
- expect(getProcedures().length).toBe(2)
413
-
414
- clear()
415
- expect(getProcedures().length).toBe(0)
416
- })
417
-
418
- test('Procedures - ctx.error still works after optimization', async () => {
419
- const { Create } = Procedures()
420
-
421
- const { ErrorTest } = Create('ErrorTest', {}, async (ctx) => {
422
- throw ctx.error('Custom error message', { code: 'ERR_001' })
423
- })
424
-
425
- try {
426
- await ErrorTest({}, {})
427
- } catch (e: any) {
428
- expect(e).toBeInstanceOf(ProcedureError)
429
- expect(e.message).toBe('Custom error message')
430
- expect(e.procedureName).toBe('ErrorTest')
431
- expect(e.meta).toEqual({ code: 'ERR_001' })
432
- }
433
- })
434
-
435
- test('Create passes through external signal from context', async () => {
436
- const { Create } = Procedures<{ signal: AbortSignal }>()
437
- const externalAc = new AbortController()
438
- let capturedSignal: AbortSignal | null = null
439
-
440
- const { WithSignal } = Create('WithSignal', {}, async (ctx) => {
441
- capturedSignal = ctx.signal!
442
- return 'done'
443
- })
444
-
445
- await WithSignal({ signal: externalAc.signal }, {})
446
- expect(capturedSignal).toBe(externalAc.signal)
447
- expect(capturedSignal!.aborted).toBe(false)
448
- })
449
-
450
- test('Create external signal reflects abort from caller', async () => {
451
- const { Create } = Procedures<{ signal: AbortSignal }>()
452
- const externalAc = new AbortController()
453
- let capturedSignal: AbortSignal | null = null
454
-
455
- const { AbortedSignal } = Create('AbortedSignal', {}, async (ctx) => {
456
- capturedSignal = ctx.signal!
457
- expect(ctx.signal!.aborted).toBe(true)
458
- return 'done'
459
- })
460
-
461
- externalAc.abort()
462
- await AbortedSignal({ signal: externalAc.signal }, {})
463
- expect(capturedSignal!.aborted).toBe(true)
464
- })
465
-
466
- test('Create external signal cancels in-flight async work', async () => {
467
- const { Create } = Procedures<{ signal: AbortSignal }>()
468
- const externalAc = new AbortController()
469
- let wasAbortedDuringWork = false
470
- const ready = Promise.withResolvers<void>()
471
-
472
- const { LongWork } = Create('LongWork', {}, async (ctx) => {
473
- ready.resolve()
474
- await new Promise<void>((resolve) => {
475
- ctx.signal!.addEventListener('abort', () => {
476
- wasAbortedDuringWork = true
477
- resolve()
478
- })
479
- })
480
- return 'done'
481
- })
482
-
483
- const p = LongWork({ signal: externalAc.signal }, {})
484
- await ready.promise
485
- externalAc.abort()
486
- await p
487
- expect(wasAbortedDuringWork).toBe(true)
488
- })
489
- })
490
-
491
- describe('Procedures - Definition Location in Errors', () => {
492
- test('ProcedureValidationError includes definition location', async () => {
493
- const { Create } = Procedures()
494
-
495
- const { TestValidation } = Create(
496
- 'TestValidation',
497
- {
498
- schema: {
499
- params: v.object({ name: v.string().required() }),
500
- },
501
- },
502
- async (ctx, params) => {
503
- return params.name
504
- }
505
- )
506
-
507
- try {
508
- // @ts-expect-error - intentionally passing invalid params
509
- await TestValidation({}, {}) // Missing required 'name' param
510
- } catch (e: any) {
511
- expect(e).toBeInstanceOf(ProcedureValidationError)
512
- expect(e.definedAt).toBeDefined()
513
- expect(e.definedAt.file).toContain('index.test.ts')
514
- expect(e.definedAt.line).toBeGreaterThan(0)
515
- expect(e.definedAt.column).toBeGreaterThan(0)
516
- expect(e.stack).toContain('--- Procedure "TestValidation" defined at ---')
517
- }
518
- })
519
-
520
- test('ctx.error() includes definition location', async () => {
521
- const { Create } = Procedures()
522
-
523
- const { TestCtxError } = Create('TestCtxError', {}, async (ctx) => {
524
- throw ctx.error('Custom error')
525
- })
526
-
527
- try {
528
- await TestCtxError({}, {})
529
- } catch (e: any) {
530
- expect(e).toBeInstanceOf(ProcedureError)
531
- expect(e.definedAt).toBeDefined()
532
- expect(e.definedAt.file).toContain('index.test.ts')
533
- expect(e.getDefinitionLocation()).toBeDefined()
534
- expect(e.stack).toContain('--- Procedure "TestCtxError" defined at ---')
535
- }
536
- })
537
-
538
- test('wrapped errors include definition location', async () => {
539
- const { Create } = Procedures()
540
-
541
- const { TestWrappedError } = Create('TestWrappedError', {}, async () => {
542
- throw new Error('Original error')
543
- })
544
-
545
- try {
546
- await TestWrappedError({}, {})
547
- } catch (e: any) {
548
- expect(e).toBeInstanceOf(ProcedureError)
549
- expect(e.definedAt).toBeDefined()
550
- expect(e.definedAt.file).toContain('index.test.ts')
551
- expect(e.message).toContain('Error in handler for TestWrappedError')
552
- expect(e.cause).toBeInstanceOf(Error)
553
- expect(e.cause.message).toBe('Original error')
554
- }
555
- })
556
-
557
- test('getDefinitionLocation returns formatted string', async () => {
558
- const { Create } = Procedures()
559
-
560
- const { TestGetLocation } = Create(
561
- 'TestGetLocation',
562
- {
563
- schema: {
564
- params: v.object({ id: v.number().required() }),
565
- },
566
- },
567
- async (ctx, params) => {
568
- return params.id
569
- }
570
- )
571
-
572
- try {
573
- // @ts-expect-error - intentionally passing invalid params
574
- await TestGetLocation({}, {}) // Missing required 'id' param
575
- } catch (e: any) {
576
- const location = e.getDefinitionLocation()
577
- expect(location).toBeDefined()
578
- expect(location).toMatch(/index\.test\.ts:\d+:\d+/)
579
- }
580
- })
581
-
582
- test('error stack shows procedure definition location at the end', async () => {
583
- const { Create } = Procedures()
584
-
585
- const { TestStackFormat } = Create(
586
- 'TestStackFormat',
587
- {
588
- schema: {
589
- params: v.object({ value: v.string().required() }),
590
- },
591
- },
592
- async (ctx, params) => {
593
- return params.value
594
- }
595
- )
596
-
597
- try {
598
- // @ts-expect-error - intentionally passing invalid params
599
- await TestStackFormat({}, {})
600
- } catch (e: any) {
601
- // Verify it's a validation error
602
- expect(e.name).toBe('ProcedureValidationError')
603
- expect(e).toBeInstanceOf(ProcedureValidationError)
604
- // Stack should contain the error message and definition location
605
- expect(e.stack).toContain('Validation error for TestStackFormat')
606
- expect(e.stack).toContain('--- Procedure "TestStackFormat" defined at ---')
607
- // The definition section should be at the end of the stack
608
- const stackLines = e.stack.split('\n')
609
- const definitionIndex = stackLines.findIndex((line: string) =>
610
- line.includes('--- Procedure "TestStackFormat" defined at ---')
611
- )
612
- expect(definitionIndex).toBeGreaterThan(0)
613
- }
614
- })
615
- })
616
-
617
- describe('Streaming Procedures - CreateStream', () => {
618
- test('CreateStream creates a streaming procedure', async () => {
619
- const { CreateStream } = Procedures()
620
-
621
- const { StreamTest, procedure, info } = CreateStream(
622
- 'StreamTest',
623
- {
624
- description: 'Test streaming procedure',
625
- schema: {
626
- params: v.object({ count: v.number().required() }),
627
- yieldType: v.object({ value: v.number().required() }),
628
- },
629
- },
630
- async function* (ctx, params) {
631
- for (let i = 0; i < params.count; i++) {
632
- yield { value: i }
633
- }
634
- }
635
- )
636
-
637
- expect(StreamTest).toBeDefined()
638
- expect(procedure).toBeDefined()
639
- expect(info.isStream).toBe(true)
640
- expect(info.name).toBe('StreamTest')
641
- expect(info.description).toBe('Test streaming procedure')
642
- expect(info.schema.params).toEqual({
643
- type: 'object',
644
- properties: { count: { type: 'number' } },
645
- required: ['count'],
646
- })
647
- expect(info.schema.yieldType).toEqual({
648
- type: 'object',
649
- properties: { value: { type: 'number' } },
650
- required: ['value'],
651
- })
652
-
653
- // Collect yielded values
654
- const values: { value: number }[] = []
655
- for await (const val of StreamTest({}, { count: 3 })) {
656
- values.push(val)
657
- }
658
-
659
- expect(values).toEqual([{ value: 0 }, { value: 1 }, { value: 2 }])
660
- })
661
-
662
- test('CreateStream validates params', async () => {
663
- const { CreateStream } = Procedures()
664
-
665
- const { StreamWithParams } = CreateStream(
666
- 'StreamWithParams',
667
- {
668
- schema: {
669
- params: v.object({ name: v.string().required() }),
670
- yieldType: v.string(),
671
- },
672
- },
673
- async function* (ctx, params) {
674
- yield params.name
675
- }
676
- )
677
-
678
- // Missing required param should throw ProcedureValidationError
679
- try {
680
- // @ts-expect-error - intentionally passing invalid params
681
- for await (const _val of StreamWithParams({}, {})) {
682
- // Should not reach here
683
- }
684
- expect.fail('Should have thrown')
685
- } catch (e: any) {
686
- expect(e).toBeInstanceOf(ProcedureValidationError)
687
- expect(e.message).toContain('Validation error for StreamWithParams')
688
- }
689
- })
690
-
691
- test('CreateStream with validateYields validates each yielded value', async () => {
692
- const { CreateStream } = Procedures()
693
- const { ProcedureYieldValidationError } = await import('./errors.js')
694
-
695
- const { StreamValidateYields } = CreateStream(
696
- 'StreamValidateYields',
697
- {
698
- schema: {
699
- yieldType: v.object({ id: v.number().required() }),
700
- },
701
- validateYields: true,
702
- },
703
- async function* () {
704
- yield { id: 1 } // Valid
705
- yield { id: 'not-a-number' } as any // Invalid - should throw
706
- }
707
- )
708
-
709
- const values: any[] = []
710
- try {
711
- for await (const val of StreamValidateYields({}, {})) {
712
- values.push(val)
713
- }
714
- expect.fail('Should have thrown on invalid yield')
715
- } catch (e: any) {
716
- expect(e).toBeInstanceOf(ProcedureYieldValidationError)
717
- expect(e.message).toContain('Yield validation error for StreamValidateYields')
718
- expect(values).toEqual([{ id: 1 }]) // First value was valid
719
- }
720
- })
721
-
722
- test('CreateStream without validateYields does not validate yields', async () => {
723
- const { CreateStream } = Procedures()
724
-
725
- const { StreamNoValidate } = CreateStream(
726
- 'StreamNoValidate',
727
- {
728
- schema: {
729
- yieldType: v.object({ id: v.number().required() }),
730
- },
731
- // validateYields defaults to false
732
- },
733
- async function* () {
734
- yield { id: 1 }
735
- yield { id: 'not-a-number' } as any // Invalid but won't throw
736
- }
737
- )
738
-
739
- const values: any[] = []
740
- for await (const val of StreamNoValidate({}, {})) {
741
- values.push(val)
742
- }
743
-
744
- expect(values).toEqual([{ id: 1 }, { id: 'not-a-number' }])
745
- })
746
-
747
- test('CreateStream ctx.error throws ProcedureError', async () => {
748
- const { CreateStream } = Procedures()
749
-
750
- const { StreamError } = CreateStream('StreamError', {}, async function* (ctx) {
751
- yield 'first'
752
- throw ctx.error('Custom stream error', { code: 'STREAM_ERR' })
753
- })
754
-
755
- const values: any[] = []
756
- try {
757
- for await (const val of StreamError({}, {})) {
758
- values.push(val)
759
- }
760
- expect.fail('Should have thrown')
761
- } catch (e: any) {
762
- expect(e).toBeInstanceOf(ProcedureError)
763
- expect(e.message).toBe('Custom stream error')
764
- expect(e.procedureName).toBe('StreamError')
765
- expect(e.meta).toEqual({ code: 'STREAM_ERR' })
766
- expect(values).toEqual(['first'])
767
- }
768
- })
769
-
770
- test('CreateStream provides ctx.signal for abort handling', async () => {
771
- const { CreateStream } = Procedures()
772
- let signalReceived = false
773
-
774
- const { StreamWithSignal } = CreateStream('StreamWithSignal', {}, async function* (ctx) {
775
- expect(ctx.signal).toBeDefined()
776
- expect(ctx.signal).toBeInstanceOf(AbortSignal)
777
- signalReceived = true
778
- yield 'value'
779
- })
780
-
781
- for await (const _val of StreamWithSignal({}, {})) {
782
- // consume
783
- }
784
-
785
- expect(signalReceived).toBe(true)
786
- })
787
-
788
- test('CreateStream appears in getProcedures with isStream flag', () => {
789
- const { Create, CreateStream, getProcedures } = Procedures()
790
-
791
- Create('RegularProc', {}, async () => 'regular')
792
-
793
- CreateStream('StreamProc', {}, async function* () {
794
- yield 'stream'
795
- })
796
-
797
- const procs = getProcedures()
798
- expect(procs.length).toBe(2)
799
-
800
- const regular = procs.find((p) => p.name === 'RegularProc')
801
- const stream = procs.find((p) => p.name === 'StreamProc')
802
-
803
- expect(regular?.isStream).toBeUndefined()
804
- expect(stream?.isStream).toBe(true)
805
- })
806
-
807
- test('CreateStream onCreate callback receives isStream flag', () =>
808
- new Promise<void>((done) => {
809
- let receivedProc: any
810
-
811
- const { CreateStream } = Procedures({
812
- onCreate: (proc) => {
813
- receivedProc = proc
814
- },
815
- })
816
-
817
- CreateStream('OnCreateStream', {}, async function* () {
818
- yield 'test'
819
- })
820
-
821
- expect(receivedProc).toBeDefined()
822
- expect(receivedProc.isStream).toBe(true)
823
- expect(receivedProc.name).toBe('OnCreateStream')
824
- done()
825
- }))
826
-
827
- test('CreateStream duplicate registration throws', () => {
828
- const { CreateStream } = Procedures()
829
-
830
- CreateStream('DuplicateStream', {}, async function* () {
831
- yield 'first'
832
- })
833
-
834
- expect(() => {
835
- CreateStream('DuplicateStream', {}, async function* () {
836
- yield 'second'
837
- })
838
- }).toThrow('Procedure with name DuplicateStream is already registered')
839
- })
840
-
841
- test('CreateStream error includes definition location', async () => {
842
- const { CreateStream } = Procedures()
843
-
844
- const { StreamErrorLocation } = CreateStream(
845
- 'StreamErrorLocation',
846
- {
847
- schema: {
848
- params: v.object({ id: v.number().required() }),
849
- },
850
- },
851
- async function* () {
852
- yield 'test'
853
- }
854
- )
855
-
856
- try {
857
- // @ts-expect-error - intentionally passing invalid params
858
- for await (const _val of StreamErrorLocation({}, {})) {
859
- // consume
860
- }
861
- expect.fail('Should have thrown')
862
- } catch (e: any) {
863
- expect(e.definedAt).toBeDefined()
864
- expect(e.definedAt.file).toContain('index.test.ts')
865
- expect(e.stack).toContain('--- Procedure "StreamErrorLocation" defined at ---')
866
- }
867
- })
868
-
869
- test('CreateStream with Typebox schema', async () => {
870
- const { CreateStream } = Procedures()
871
-
872
- const { TypeboxStream } = CreateStream(
873
- 'TypeboxStream',
874
- {
875
- schema: {
876
- params: Type.Object({ limit: Type.Number() }),
877
- yieldType: Type.Object({ data: Type.String() }),
878
- },
879
- },
880
- async function* (ctx, params) {
881
- for (let i = 0; i < params.limit; i++) {
882
- yield { data: `item-${i}` }
883
- }
884
- }
885
- )
886
-
887
- const values: any[] = []
888
- for await (const val of TypeboxStream({}, { limit: 2 })) {
889
- values.push(val)
890
- }
891
-
892
- expect(values).toEqual([{ data: 'item-0' }, { data: 'item-1' }])
893
- })
894
-
895
- test('CreateStream with context type', async () => {
896
- interface StreamContext {
897
- userId: string
898
- }
899
-
900
- const { CreateStream } = Procedures<StreamContext>()
901
-
902
- const { ContextStream } = CreateStream('ContextStream', {}, async function* (ctx) {
903
- // ctx should have both userId and error
904
- expect(ctx.userId).toBe('user-123')
905
- expect(ctx.error).toBeDefined()
906
- expect(ctx.signal).toBeDefined()
907
- yield ctx.userId
908
- })
909
-
910
- const values: any[] = []
911
- for await (const val of ContextStream({ userId: 'user-123' }, {})) {
912
- values.push(val)
913
- }
914
-
915
- expect(values).toEqual(['user-123'])
916
- })
917
-
918
- test('CreateStream wrapped errors preserve cause', async () => {
919
- const { CreateStream } = Procedures()
920
- const originalError = new Error('Stream underlying error')
921
- ;(originalError as any).code = 'STREAM_FAIL'
922
-
923
- const { StreamCause } = CreateStream(
924
- 'StreamCause',
925
- {},
926
- // eslint-disable-next-line require-yield
927
- async function* () {
928
- throw originalError
929
- }
930
- )
931
-
932
- try {
933
- for await (const _val of StreamCause({}, {})) {
934
- // consume
935
- }
936
- expect.fail('Should have thrown')
937
- } catch (e: any) {
938
- expect(e).toBeInstanceOf(ProcedureError)
939
- expect(e.cause).toBe(originalError)
940
- expect(e.cause.code).toBe('STREAM_FAIL')
941
- }
942
- })
943
-
944
- test('CreateStream with extended config', () => {
945
- interface ExtConfig {
946
- scope: string
947
- version: number
948
- }
949
-
950
- const { CreateStream } = Procedures<unknown, ExtConfig>()
951
-
952
- const { info } = CreateStream(
953
- 'ExtendedStream',
954
- {
955
- scope: 'api',
956
- version: 1,
957
- description: 'Extended config stream',
958
- },
959
- async function* () {
960
- yield 'data'
961
- }
962
- )
963
-
964
- expect(info.scope).toBe('api')
965
- expect(info.version).toBe(1)
966
- expect(info.description).toBe('Extended config stream')
967
- expect(info.isStream).toBe(true)
968
- })
969
-
970
- test('CreateStream signal.aborted becomes true after generator completes', async () => {
971
- const { CreateStream } = Procedures()
972
- let capturedSignal: AbortSignal | null = null
973
-
974
- const { AbortStream } = CreateStream('AbortStream', {}, async function* (ctx) {
975
- capturedSignal = ctx.signal
976
- yield 'value'
977
- })
978
-
979
- // Consume the generator
980
- for await (const _val of AbortStream({}, {})) {
981
- // consume
982
- }
983
-
984
- // After generator completes, signal should be aborted
985
- expect(capturedSignal).not.toBeNull()
986
- expect(capturedSignal!.aborted).toBe(true)
987
- })
988
-
989
- test('CreateStream signal.reason is stream-completed after normal completion', async () => {
990
- const { CreateStream } = Procedures()
991
- let capturedSignal: AbortSignal | null = null
992
-
993
- const { ReasonStream } = CreateStream('ReasonStream', {}, async function* (ctx) {
994
- capturedSignal = ctx.signal
995
- yield 'value'
996
- })
997
-
998
- for await (const _val of ReasonStream({}, {})) {
999
- // consume
1000
- }
1001
-
1002
- expect(capturedSignal!.reason).toBe('stream-completed')
1003
- })
1004
-
1005
- test('CreateStream combines external signal with internal signal', async () => {
1006
- const { CreateStream } = Procedures<{ signal: AbortSignal }>()
1007
- const externalAc = new AbortController()
1008
- let capturedSignal: AbortSignal | null = null
1009
-
1010
- const { CombinedStream } = CreateStream('CombinedStream', {}, async function* (ctx) {
1011
- capturedSignal = ctx.signal
1012
- // Combined signal is a new object, not the raw external signal
1013
- expect(ctx.signal).not.toBe(externalAc.signal)
1014
- yield 'value'
1015
- })
1016
-
1017
- // Abort external before consuming — combined signal should reflect it
1018
- externalAc.abort('client-disconnected')
1019
-
1020
- for await (const _val of CombinedStream({ signal: externalAc.signal }, {})) {
1021
- // consume
1022
- }
1023
-
1024
- expect(capturedSignal!.aborted).toBe(true)
1025
- // Reason comes from external abort, not internal 'stream-completed'
1026
- expect(capturedSignal!.reason).toBe('client-disconnected')
1027
- })
1028
- })
1029
-
1030
- describe('isPrevalidated context property', () => {
1031
- test('Create skips validation when ctx.isPrevalidated is true', async () => {
1032
- const { Create } = Procedures()
1033
-
1034
- const { SkipValidation } = Create(
1035
- 'SkipValidation',
1036
- {
1037
- schema: {
1038
- params: v.object({ name: v.string().required() }),
1039
- },
1040
- },
1041
- async (ctx, params) => {
1042
- return params
1043
- }
1044
- )
1045
-
1046
- // Without isPrevalidated, missing required param would throw
1047
- // With isPrevalidated: true, validation is skipped
1048
- const result = await SkipValidation({ isPrevalidated: true }, {} as any)
1049
- expect(result).toEqual({})
1050
- })
1051
-
1052
- test('Create validates when ctx.isPrevalidated is false', async () => {
1053
- const { Create } = Procedures()
1054
-
1055
- const { ValidateParams } = Create(
1056
- 'ValidateParams',
1057
- {
1058
- schema: {
1059
- params: v.object({ name: v.string().required() }),
1060
- },
1061
- },
1062
- async (ctx, params) => {
1063
- return params
1064
- }
1065
- )
1066
-
1067
- // With isPrevalidated: false, validation should still run
1068
- try {
1069
- await ValidateParams({ isPrevalidated: false }, {} as any)
1070
- expect.fail('Should have thrown validation error')
1071
- } catch (e: any) {
1072
- expect(e).toBeInstanceOf(ProcedureValidationError)
1073
- }
1074
- })
1075
-
1076
- test('Create validates when ctx.isPrevalidated is undefined', async () => {
1077
- const { Create } = Procedures()
1078
-
1079
- const { ValidateUndefined } = Create(
1080
- 'ValidateUndefined',
1081
- {
1082
- schema: {
1083
- params: v.object({ id: v.number().required() }),
1084
- },
1085
- },
1086
- async (ctx, params) => {
1087
- return params
1088
- }
1089
- )
1090
-
1091
- // Without isPrevalidated property, validation should run
1092
- try {
1093
- await ValidateUndefined({}, {} as any)
1094
- expect.fail('Should have thrown validation error')
1095
- } catch (e: any) {
1096
- expect(e).toBeInstanceOf(ProcedureValidationError)
1097
- }
1098
- })
1099
-
1100
- test('CreateStream skips validation when ctx.isPrevalidated is true', async () => {
1101
- const { CreateStream } = Procedures()
1102
-
1103
- const { StreamSkipValidation } = CreateStream(
1104
- 'StreamSkipValidation',
1105
- {
1106
- schema: {
1107
- params: v.object({ count: v.number().required() }),
1108
- },
1109
- },
1110
- async function* (ctx, params) {
1111
- yield { received: params }
1112
- }
1113
- )
1114
-
1115
- // With isPrevalidated: true, validation is skipped even with invalid params
1116
- const values: any[] = []
1117
- for await (const val of StreamSkipValidation({ isPrevalidated: true }, {} as any)) {
1118
- values.push(val)
1119
- }
1120
-
1121
- expect(values).toEqual([{ received: {} }])
1122
- })
1123
-
1124
- test('CreateStream validates when ctx.isPrevalidated is false', async () => {
1125
- const { CreateStream } = Procedures()
1126
-
1127
- const { StreamValidate } = CreateStream(
1128
- 'StreamValidate',
1129
- {
1130
- schema: {
1131
- params: v.object({ count: v.number().required() }),
1132
- },
1133
- },
1134
- async function* (ctx, params) {
1135
- yield { received: params }
1136
- }
1137
- )
1138
-
1139
- // With isPrevalidated: false, validation should run
1140
- try {
1141
- for await (const _val of StreamValidate({ isPrevalidated: false }, {} as any)) {
1142
- // consume
1143
- }
1144
- expect.fail('Should have thrown validation error')
1145
- } catch (e: any) {
1146
- expect(e).toBeInstanceOf(ProcedureValidationError)
1147
- }
1148
- })
1149
-
1150
- test('CreateStream validates when ctx.isPrevalidated is undefined', async () => {
1151
- const { CreateStream } = Procedures()
1152
-
1153
- const { StreamValidateUndefined } = CreateStream(
1154
- 'StreamValidateUndefined',
1155
- {
1156
- schema: {
1157
- params: v.object({ value: v.string().required() }),
1158
- },
1159
- },
1160
- async function* (ctx, params) {
1161
- yield params
1162
- }
1163
- )
1164
-
1165
- // Without isPrevalidated property, validation should run
1166
- try {
1167
- for await (const _val of StreamValidateUndefined({}, {} as any)) {
1168
- // consume
1169
- }
1170
- expect.fail('Should have thrown validation error')
1171
- } catch (e: any) {
1172
- expect(e).toBeInstanceOf(ProcedureValidationError)
1173
- }
1174
- })
1175
-
1176
- test('isPrevalidated is available in handler context', async () => {
1177
- const { Create } = Procedures()
1178
- let capturedIsPrevalidated: boolean | undefined
1179
-
1180
- const { CapturePrevalidated } = Create('CapturePrevalidated', {}, async (ctx) => {
1181
- capturedIsPrevalidated = ctx.isPrevalidated
1182
- return 'done'
1183
- })
1184
-
1185
- await CapturePrevalidated({ isPrevalidated: true }, {})
1186
- expect(capturedIsPrevalidated).toBe(true)
1187
-
1188
- await CapturePrevalidated({ isPrevalidated: false }, {})
1189
- expect(capturedIsPrevalidated).toBe(false)
1190
-
1191
- await CapturePrevalidated({}, {})
1192
- expect(capturedIsPrevalidated).toBeUndefined()
1193
- })
1194
- })